mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-11-12 18:25:28 +01:00
Compare commits
104
Commits
No files matched your search
Generated
+187
@@ -0,0 +1,187 @@
|
||||
## 2026-11-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
|
||||
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
|
||||
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
|
||||
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
|
||||
|
||||
## 2026-11-06
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- UmbrelOS-VM: report runs as ProxmoxVE, not DevScripts [@MickLesk](https://github.com/MickLesk) ([#18497](https://github.com/community-scripts/ProxmoxVE/pull/18497))
|
||||
- Caddy: skip the xCaddy test build below 2 GB RAM [@MickLesk](https://github.com/MickLesk) ([#18472](https://github.com/community-scripts/ProxmoxVE/pull/18472))
|
||||
- Thingsboard: keep the package defaults in thingsboard.conf [@MickLesk](https://github.com/MickLesk) ([#18499](https://github.com/community-scripts/ProxmoxVE/pull/18499))
|
||||
- Nexterm: keep the extracted app out of the /tmp cleanup [@MickLesk](https://github.com/MickLesk) ([#18501](https://github.com/community-scripts/ProxmoxVE/pull/18501))
|
||||
- PLANKA: run the service with node instead of nodemon [@MickLesk](https://github.com/MickLesk) ([#18503](https://github.com/community-scripts/ProxmoxVE/pull/18503))
|
||||
- MeTube/Yubal: install Deno without its shell setup [@MickLesk](https://github.com/MickLesk) ([#18406](https://github.com/community-scripts/ProxmoxVE/pull/18406))
|
||||
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Technitium DNS: read the version from its GitHub releases [@MickLesk](https://github.com/MickLesk) ([#18425](https://github.com/community-scripts/ProxmoxVE/pull/18425))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
|
||||
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- ci: reject DevScripts URLs in scripts [@MickLesk](https://github.com/MickLesk) ([#18498](https://github.com/community-scripts/ProxmoxVE/pull/18498))
|
||||
|
||||
## 2026-11-05
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
|
||||
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
|
||||
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
|
||||
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
|
||||
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
|
||||
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
|
||||
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: keep the existing install until the new archive has extracted [@rcourtman](https://github.com/rcourtman) ([core#201](https://github.com/community-scripts/core/pull/201))
|
||||
- fix: keep the install when a clone or an Alpine extract fails [@rcourtman](https://github.com/rcourtman) ([core#210](https://github.com/community-scripts/core/pull/210))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Arcane: write both env names, stop shipping the unused .env [@MickLesk](https://github.com/MickLesk) ([#18466](https://github.com/community-scripts/ProxmoxVE/pull/18466))
|
||||
|
||||
## 2026-11-04
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Teleport: Poll readyz instead of fixed 10 second sleep. [@cthach](https://github.com/cthach) ([#18461](https://github.com/community-scripts/ProxmoxVE/pull/18461))
|
||||
- n8n: install past its unsatisfiable canvas dependencies [@MickLesk](https://github.com/MickLesk) ([#18439](https://github.com/community-scripts/ProxmoxVE/pull/18439))
|
||||
- Teable: add Redis cache [@MickLesk](https://github.com/MickLesk) ([#18450](https://github.com/community-scripts/ProxmoxVE/pull/18450))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: say why download_gpg_key failed [@MickLesk](https://github.com/MickLesk) ([core#208](https://github.com/community-scripts/core/pull/208))
|
||||
- feat: read the MOTD's OS version at login [@MickLesk](https://github.com/MickLesk) ([core#207](https://github.com/community-scripts/core/pull/207))
|
||||
- feat: skip screen clear when there is no terminal | pin only the scripts own app with var_appversion [@MickLesk](https://github.com/MickLesk) ([core#203](https://github.com/community-scripts/core/pull/203))
|
||||
|
||||
## 2026-11-03
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fedora VM: let the engine install libguestfs-tools [@MickLesk](https://github.com/MickLesk) ([#18422](https://github.com/community-scripts/ProxmoxVE/pull/18422))
|
||||
- Cinephage: run the camoufox binary the package now ships [@MickLesk](https://github.com/MickLesk) ([#18426](https://github.com/community-scripts/ProxmoxVE/pull/18426))
|
||||
- AlmaLinux VM: report failures through the engine's error handler [@MickLesk](https://github.com/MickLesk) ([#18418](https://github.com/community-scripts/ProxmoxVE/pull/18418))
|
||||
- WireGuard: give the Debian container 1 GB for WGDashboard [@MickLesk](https://github.com/MickLesk) ([#18419](https://github.com/community-scripts/ProxmoxVE/pull/18419))
|
||||
- zabbix: use os specific apt packages [@MickLesk](https://github.com/MickLesk) ([#18417](https://github.com/community-scripts/ProxmoxVE/pull/18417))
|
||||
- Discourse: build libvips 8.18 for its image worker [@MickLesk](https://github.com/MickLesk) ([#18430](https://github.com/community-scripts/ProxmoxVE/pull/18430))
|
||||
- Wazuh: show the installer's own error when it fails [@MickLesk](https://github.com/MickLesk) ([#18428](https://github.com/community-scripts/ProxmoxVE/pull/18428))
|
||||
- Tracearr: move to Node.js 26 [@connorgallopo](https://github.com/connorgallopo) ([#18393](https://github.com/community-scripts/ProxmoxVE/pull/18393))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Wiki.js: install 3.x [@MickLesk](https://github.com/MickLesk) ([#18382](https://github.com/community-scripts/ProxmoxVE/pull/18382))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- OpenWrt VM: read the stable release from downloads.openwrt.org [@MickLesk](https://github.com/MickLesk) ([#18411](https://github.com/community-scripts/ProxmoxVE/pull/18411))
|
||||
- Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first [@MickLesk](https://github.com/MickLesk) ([#18397](https://github.com/community-scripts/ProxmoxVE/pull/18397))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: put the reason for a VM failure into its trace [@MickLesk](https://github.com/MickLesk) ([core#200](https://github.com/community-scripts/core/pull/200))
|
||||
- fix: check the bridge before creating a container, and its state after start [@MickLesk](https://github.com/MickLesk) ([core#199](https://github.com/community-scripts/core/pull/199))
|
||||
- ref: offer to remove only the container this run created [@MickLesk](https://github.com/MickLesk) ([core#198](https://github.com/community-scripts/core/pull/198))
|
||||
- feat: report a cancelled tool dialog as an abort and name the failing command [@MickLesk](https://github.com/MickLesk) ([core#197](https://github.com/community-scripts/core/pull/197))
|
||||
- feat: check KVM and the host's threads before building a VM [@MickLesk](https://github.com/MickLesk) ([core#196](https://github.com/community-scripts/core/pull/196))
|
||||
- ref: give a host tool one log file for the whole run [@MickLesk](https://github.com/MickLesk) ([core#194](https://github.com/community-scripts/core/pull/194))
|
||||
- fix: keep a piped stdin when output moves to the log after a hangup [@MickLesk](https://github.com/MickLesk) ([core#193](https://github.com/community-scripts/core/pull/193))
|
||||
- fix: pve-LXC: say when the container went away during its setup [@MickLesk](https://github.com/MickLesk) ([core#192](https://github.com/community-scripts/core/pull/192))
|
||||
- fix: treat Esc in a VM wizard dialog as leaving the script [@MickLesk](https://github.com/MickLesk) ([core#190](https://github.com/community-scripts/core/pull/190))
|
||||
- ref: count a container stopped during a detached install as an abort [@MickLesk](https://github.com/MickLesk) ([core#187](https://github.com/community-scripts/core/pull/187))
|
||||
- feat: find the assets of a renamed GitHub repo without the API [@MickLesk](https://github.com/MickLesk) ([core#188](https://github.com/community-scripts/core/pull/188))
|
||||
- fix: restore npm when an interrupted update left it behind [@MickLesk](https://github.com/MickLesk) ([core#189](https://github.com/community-scripts/core/pull/189))
|
||||
- feat: add var_cmode to set the container console mode [@MickLesk](https://github.com/MickLesk) ([core#180](https://github.com/community-scripts/core/pull/180))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- all-templates: answering No or Cancel is not a failure [@MickLesk](https://github.com/MickLesk) ([#18423](https://github.com/community-scripts/ProxmoxVE/pull/18423))
|
||||
- homebrew: skip service accounts when picking the user, survive a failed apt update [@MickLesk](https://github.com/MickLesk) ([#18424](https://github.com/community-scripts/ProxmoxVE/pull/18424))
|
||||
- add-tailscale-lxc: start a stopped container and check its OS first [@MickLesk](https://github.com/MickLesk) ([#18421](https://github.com/community-scripts/ProxmoxVE/pull/18421))
|
||||
- netdata: survive a failed apt update, leave quietly on Cancel [@MickLesk](https://github.com/MickLesk) ([#18427](https://github.com/community-scripts/ProxmoxVE/pull/18427))
|
||||
- Privilege-Converter: offer only container-capable storages as the converter's restore target [@MickLesk](https://github.com/MickLesk) ([#18415](https://github.com/community-scripts/ProxmoxVE/pull/18415))
|
||||
- let post-pve-install go on when legacy sources are kept [@MickLesk](https://github.com/MickLesk) ([#18403](https://github.com/community-scripts/ProxmoxVE/pull/18403))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- update-apps: say why an backup or restore failed [@MickLesk](https://github.com/MickLesk) ([#18405](https://github.com/community-scripts/ProxmoxVE/pull/18405))
|
||||
- Keep host tools running when clear does not know the terminal [@MickLesk](https://github.com/MickLesk) ([#18412](https://github.com/community-scripts/ProxmoxVE/pull/18412))
|
||||
- Refuse to run the PBS 4 upgrade on a Proxmox VE host [@MickLesk](https://github.com/MickLesk) ([#18416](https://github.com/community-scripts/ProxmoxVE/pull/18416))
|
||||
- fstrim: keep trimming when pct fstrim fails on one container [@MickLesk](https://github.com/MickLesk) ([#18413](https://github.com/community-scripts/ProxmoxVE/pull/18413))
|
||||
- execute-lxc: do not abort on a container that is not Debian or Ubuntu [@MickLesk](https://github.com/MickLesk) ([#18414](https://github.com/community-scripts/ProxmoxVE/pull/18414))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- github: flag instead of closing on first PR template miss; add script-request eligibility check [@MickLesk](https://github.com/MickLesk) ([#18384](https://github.com/community-scripts/ProxmoxVE/pull/18384))
|
||||
|
||||
## 2026-11-02
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- FileFlows: replace the program folders on update [@MickLesk](https://github.com/MickLesk) ([#18380](https://github.com/community-scripts/ProxmoxVE/pull/18380))
|
||||
- anchor: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18373](https://github.com/community-scripts/ProxmoxVE/pull/18373))
|
||||
- guardian: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18375](https://github.com/community-scripts/ProxmoxVE/pull/18375))
|
||||
- nametag: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18376](https://github.com/community-scripts/ProxmoxVE/pull/18376))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- github: trim theater, add signal to the engine bug report; move AI block to end of PR template [@MickLesk](https://github.com/MickLesk) ([core#186](https://github.com/community-scripts/core/pull/186))
|
||||
- fix: do not wait forever for verbose output a daemon keeps open [@MickLesk](https://github.com/MickLesk) ([core#185](https://github.com/community-scripts/core/pull/185))
|
||||
|
||||
## 2026-11-01
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Amazon Linux ([#18369](https://github.com/community-scripts/ProxmoxVE/pull/18369))
|
||||
- CachyOS VM ([#18368](https://github.com/community-scripts/ProxmoxVE/pull/18368))
|
||||
- Razzia ([#18366](https://github.com/community-scripts/ProxmoxVE/pull/18366))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(zerobyte): build the bundled local agent [@nicotsx](https://github.com/nicotsx) ([#18363](https://github.com/community-scripts/ProxmoxVE/pull/18363))
|
||||
Generated
+114
@@ -0,0 +1,114 @@
|
||||
name: ARM script test (manual)
|
||||
|
||||
# Builds one ct/ script on GitHub's free arm64 runner (ubuntu-24.04-arm),
|
||||
# inside Incus, with the same harness as pr-script-test.yml. Started by hand
|
||||
# from the Actions tab; it posts nothing anywhere: the result is in the job
|
||||
# summary and the artifact (with the screenshots).
|
||||
#
|
||||
# pr-script-test.yml runs the same ARM build for every /test, as information
|
||||
# only. This one is for trying a script or branch on ARM without a PR.
|
||||
#
|
||||
# Public repositories get the standard hosted runners, arm64 included, at no
|
||||
# cost. Each job is a fresh VM that is thrown away afterwards, so the script
|
||||
# under test never touches the lab.
|
||||
#
|
||||
# The engine refuses to guess on arm64: a script that leaves var_arm64 unset
|
||||
# stops with "refusing to guess". Here var_arm64=no means "not built", yes is
|
||||
# built as is, and unset is built with var_arm64=yes to see whether it would
|
||||
# work.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
script:
|
||||
description: "ct/ script, e.g. ct/bark-server.sh"
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: "Branch or full SHA of this repository"
|
||||
required: false
|
||||
default: main
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
# The harness from the default branch, as in pr-script-test.yml.
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .github/workflows/scripts
|
||||
persist-credentials: false
|
||||
|
||||
- name: Decide var_arm64
|
||||
id: plan
|
||||
env:
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
REF: ${{ inputs.ref }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
[[ "$SCRIPT" =~ ^ct/[A-Za-z0-9._-]+\.sh$ ]] || { echo "::error::script must be ct/<name>.sh"; exit 1; }
|
||||
[[ "$REF" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "::error::bad ref"; exit 1; }
|
||||
sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)
|
||||
text=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${SCRIPT}?ref=${sha}" --jq .content | base64 -d)
|
||||
# A commented-out line ("#var_arm64=...") counts as unset.
|
||||
arm=$(grep -m1 -oE '^[[:space:]]*var_arm64="[$][{]var_arm64:-(yes|no)[}]"' <<<"$text" | grep -oE ':-(yes|no)' | cut -c3- || true)
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
echo "arm64=${arm:-unset}" >>"$GITHUB_OUTPUT"
|
||||
case "$arm" in
|
||||
no) echo "::notice::${SCRIPT} sets var_arm64=no: not built on ARM" ;;
|
||||
yes) echo "${SCRIPT} sets var_arm64=yes" ;;
|
||||
*) echo "::notice::${SCRIPT} leaves var_arm64 unset: building with var_arm64=yes to see whether it works" ;;
|
||||
esac
|
||||
echo "Building ${SCRIPT} at ${sha:0:7} on $(uname -m), $(nproc) cores, $(free -g | awk '/^Mem/ {print $2}') GB RAM, $(df -h / | awk 'NR==2 {print $4}') free"
|
||||
|
||||
- name: Set up Incus
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
|
||||
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
|
||||
|
||||
- name: Build and check ${{ inputs.script }}
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
PLATFORM: incus
|
||||
VAR_ARM64: "yes"
|
||||
PR_REPO: ${{ github.repository }}
|
||||
PR_SHA: ${{ steps.plan.outputs.sha }}
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
UPSTREAM: ${{ github.repository }}
|
||||
CTID_BASE: "9000"
|
||||
GH_API_TOKEN: ${{ github.token }}
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
BUILD_TIMEOUT: 90m
|
||||
run: sudo --preserve-env=PLATFORM,VAR_ARM64,PR_REPO,PR_SHA,SCRIPT,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,BUILD_TIMEOUT,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT bash .github/workflows/scripts/pve-run-script.sh
|
||||
|
||||
- name: Summary
|
||||
if: always() && steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
ARM64: ${{ steps.plan.outputs.arm64 }}
|
||||
run: |
|
||||
sudo chmod -R a+rX "$OUT_DIR" 2>/dev/null || true
|
||||
r="$OUT_DIR/result.json"
|
||||
[[ -f "$r" ]] || { echo "No result.json" >>"$GITHUB_STEP_SUMMARY"; exit 0; }
|
||||
{
|
||||
echo "### ARM (arm64, Incus): $(jq -r .script "$r")"
|
||||
echo
|
||||
echo "| Status | Install | Update | Duration | var_arm64 | Detail |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
jq -r --arg arm "$ARM64" '"| \(.status) | \(if .update_status != "" then "pass" else .status end) | \(if .update_status == "" then "-" else .update_status end) | \(.duration)s | \($arm) | \(if .status == "pass" then (.url // "" | if . == "" then "no URL" else . + " → " + $ARGS.named.c end) else .step end) |"' --arg c "$(jq -r .http_code "$r")" "$r"
|
||||
echo
|
||||
ls "$OUT_DIR"/screenshot*.png &>/dev/null && echo "Screenshots: in the \`result-arm\` artifact." || echo "No screenshot."
|
||||
} >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: result-arm
|
||||
path: ${{ runner.temp }}/result
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
+13
-6
@@ -230,7 +230,7 @@ jobs:
|
||||
|
||||
for (const slug of slugs) {
|
||||
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1&expand=type', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
const list = JSON.parse(listRes.body);
|
||||
@@ -239,14 +239,21 @@ jobs:
|
||||
console.log('Slug not in DB, skipping: ' + slug);
|
||||
continue;
|
||||
}
|
||||
const patch = {
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
};
|
||||
// Containers in ProxmoxVE are synced to Incus from here on.
|
||||
const recordType = record.expand && record.expand.type && record.expand.type.type;
|
||||
const platforms = Array.isArray(record.platforms) ? record.platforms : [];
|
||||
if (recordType === 'lxc' && !platforms.includes('incus')) patch.platforms = ['pve', 'incus'];
|
||||
else if (!platforms.length) patch.platforms = ['pve'];
|
||||
if (!Array.isArray(record.architectures) || !record.architectures.length) patch.architectures = ['amd64'];
|
||||
const patchRes = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
})
|
||||
body: JSON.stringify(patch)
|
||||
});
|
||||
if (!patchRes.ok) {
|
||||
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
|
||||
|
||||
Generated
+149
-4
@@ -2,7 +2,7 @@ name: PocketBase Bot
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -25,6 +25,9 @@ jobs:
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
SENDER: ${{ github.event.sender.login }}
|
||||
PREVIOUS_BODY: ${{ github.event.changes.body.from }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || 'false' }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
@@ -87,7 +90,7 @@ jobs:
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
|
||||
const isPrComment = process.env.IS_PR_COMMENT === 'true';
|
||||
const commentId = parseInt(process.env.COMMENT_ID, 10);
|
||||
const actor = process.env.ACTOR;
|
||||
let actor = process.env.ACTOR;
|
||||
|
||||
function ghRequest(path, method, body) {
|
||||
const headers = {
|
||||
@@ -278,7 +281,65 @@ jobs:
|
||||
// Accept /pocketbase at the start of ANY line (leading whitespace ok),
|
||||
// so the command works even when preceded by other text. Mid-sentence
|
||||
// mentions and blockquoted ("> ...") examples are ignored.
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
let commentBody = process.env.COMMENT_BODY || '';
|
||||
|
||||
// ── Metadata requests from the website ─────────────────────────────
|
||||
// One comment per requested change, each with Accept/Decline boxes.
|
||||
// Ticking one edits the comment; that edit is the only event handled.
|
||||
const REQUEST_MARKER = '<!-- pb-request -->';
|
||||
const DONE_MARKER = '<!-- pb-request:done -->';
|
||||
const isRequest = commentBody.includes(REQUEST_MARKER);
|
||||
let decidedBy = null;
|
||||
if (process.env.EVENT_ACTION === 'edited' && !isRequest) process.exit(0);
|
||||
if (isRequest) {
|
||||
if (process.env.EVENT_ACTION !== 'edited' || commentBody.includes(DONE_MARKER)) process.exit(0);
|
||||
const ticked = (body, word) =>
|
||||
new RegExp('^[ \\t]*[-*] \\[[xX]\\] ' + word + '[ \\t]*\\r?$', 'm').test(body);
|
||||
const previous = process.env.PREVIOUS_BODY || '';
|
||||
const accept = ticked(commentBody, 'Accept') && !ticked(previous, 'Accept');
|
||||
const decline = ticked(commentBody, 'Decline') && !ticked(previous, 'Decline');
|
||||
if (accept === decline) process.exit(0);
|
||||
|
||||
const sender = process.env.SENDER;
|
||||
const permRes = await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/collaborators/' + encodeURIComponent(sender) + '/permission'
|
||||
);
|
||||
const level = permRes.ok ? JSON.parse(permRes.body).permission : 'none';
|
||||
if (level !== 'admin' && level !== 'write') {
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: @' + sender + ' is not authorized to decide metadata requests.\n' +
|
||||
'Only maintainers with write access can accept or decline them.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const heading = (commentBody.match(/^### (.+)$/m) || [])[1] || 'this change';
|
||||
const command = commentBody.match(/<!-- pb-command -->\r?\n([\s\S]*?)\r?\n<!-- \/pb-command -->/);
|
||||
const decided = commentBody
|
||||
.replace(/^[ \t]*[-*] \[[ xX]\] (Accept|Decline)[ \t]*\r?\n?/gm, '')
|
||||
.replace(
|
||||
REQUEST_MARKER,
|
||||
'**' + (accept ? '✅ Accepted' : '❌ Declined') + '** by @' + sender + '\n\n' +
|
||||
REQUEST_MARKER + '\n' + DONE_MARKER
|
||||
);
|
||||
const markRes = await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId, 'PATCH', { body: decided }
|
||||
);
|
||||
if (!markRes.ok) console.warn('Could not mark the request as decided:', markRes.body);
|
||||
|
||||
if (decline) {
|
||||
await postComment('❌ **PocketBase Bot**: **' + heading + '** declined by @' + sender + '.');
|
||||
process.exit(0);
|
||||
}
|
||||
if (!command) {
|
||||
await postComment('❌ **PocketBase Bot**: no command found in the request for **' + heading + '**.');
|
||||
process.exit(0);
|
||||
}
|
||||
commentBody = command[1];
|
||||
decidedBy = sender;
|
||||
actor = sender;
|
||||
}
|
||||
|
||||
const cmdLine = commentBody
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
@@ -292,7 +353,7 @@ jobs:
|
||||
|
||||
// ── Permission check ───────────────────────────────────────────────
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
if (!decidedBy && association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
|
||||
@@ -957,6 +1018,35 @@ jobs:
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
if (fieldName === 'app_vars') {
|
||||
const TYPES = ['text', 'password', 'number', 'boolean', 'select'];
|
||||
const problems = [];
|
||||
if (!Array.isArray(parsedJson)) problems.push('must be a JSON array');
|
||||
else parsedJson.forEach(function (v, i) {
|
||||
const at = 'entry ' + (i + 1) + ': ';
|
||||
if (!v || typeof v !== 'object' || Array.isArray(v)) { problems.push(at + 'must be an object'); return; }
|
||||
if (typeof v.name !== 'string' || !/^var_[a-z0-9_]+$/.test(v.name)) problems.push(at + '`name` must look like `var_example`');
|
||||
if (typeof v.label !== 'string' || !v.label.trim()) problems.push(at + '`label` is required');
|
||||
if (TYPES.indexOf(v.type) === -1) problems.push(at + '`type` must be one of ' + TYPES.join(', '));
|
||||
if (v.default !== undefined && typeof v.default !== 'string') problems.push(at + '`default` must be a string');
|
||||
if (v.type === 'select') {
|
||||
const opts = v.options;
|
||||
if (!Array.isArray(opts) || opts.length === 0 || opts.some(function (o) { return typeof o !== 'string' || !o; })) {
|
||||
problems.push(at + 'a select needs `options`, a non-empty array of strings');
|
||||
} else if (v.default !== undefined && opts.indexOf(v.default) === -1) {
|
||||
problems.push(at + '`default` must be one of its options');
|
||||
}
|
||||
}
|
||||
['required', 'secret'].forEach(function (k) {
|
||||
if (v[k] !== undefined && typeof v[k] !== 'boolean') problems.push(at + '`' + k + '` must be true or false');
|
||||
});
|
||||
});
|
||||
if (problems.length > 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: `app_vars` was not saved.\n\n- ' + problems.join('\n- '));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
setPayload[fieldName] = parsedJson;
|
||||
} else {
|
||||
setPayload[fieldName] = codeBlockValue;
|
||||
@@ -1236,3 +1326,58 @@ jobs:
|
||||
});
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
|
||||
- name: Close a metadata request once every change is decided
|
||||
if: always() && github.event.action == 'edited' && contains(github.event.comment.body, '<!-- pb-request -->')
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
const api = 'https://api.github.com/repos/' + process.env.REPO + '/issues/' + process.env.ISSUE_NUMBER;
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'pocketbase-bot'
|
||||
};
|
||||
const https = require('https');
|
||||
function call(method, url, body) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const req = https.request(url, { method: method, headers: headers }, function (res) {
|
||||
let data = '';
|
||||
res.on('data', function (d) { data += d; });
|
||||
res.on('end', function () { resolve({ ok: res.statusCode < 300, status: res.statusCode, body: data }); });
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
const requests = [];
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const res = await call('GET', api + '/comments?per_page=100&page=' + page);
|
||||
if (!res.ok) throw new Error('Could not list comments: ' + res.status);
|
||||
const comments = JSON.parse(res.body);
|
||||
for (const c of comments) {
|
||||
if ((c.body || '').includes('<!-- pb-request -->')) requests.push(c.body);
|
||||
}
|
||||
if (comments.length < 100) break;
|
||||
}
|
||||
const open = requests.filter(function (b) { return !b.includes('<!-- pb-request:done -->'); });
|
||||
if (requests.length === 0 || open.length > 0) {
|
||||
console.log(open.length + ' of ' + requests.length + ' changes still undecided.');
|
||||
return;
|
||||
}
|
||||
const accepted = requests.filter(function (b) { return b.includes('✅ Accepted'); }).length;
|
||||
const res = await call('PATCH', api, { state: 'closed', state_reason: accepted > 0 ? 'completed' : 'not_planned' });
|
||||
if (!res.ok) throw new Error('Could not close the issue: ' + res.status);
|
||||
console.log('Closed: ' + accepted + ' accepted, ' + (requests.length - accepted) + ' declined.');
|
||||
})().catch(function (e) {
|
||||
console.error(e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
Generated
+840
@@ -0,0 +1,840 @@
|
||||
name: PR script test
|
||||
|
||||
# Builds the ct/ and vm/ scripts a pull request touches on real Proxmox and Incus hosts and
|
||||
# reports the outcome on the PR, as a check run and as one comment kept up to
|
||||
# date.
|
||||
#
|
||||
# Every push to a non-draft PR is built automatically, forks included: the
|
||||
# maintainers decided (2026-11-12) that the lab hosts run contributors' code
|
||||
# unattended. They are isolated, hold no secret, and are treated as
|
||||
# compromised anyway; the sweeper and fresh guests per build keep them usable.
|
||||
# Someone with write access can also comment `/test` (optionally followed by
|
||||
# a full SHA to pin, the scripts to build, and the platforms to build on) to
|
||||
# build again or pick what to build. Only a commit that existed when the
|
||||
# comment was written gets built: a push after it is never covered, not even
|
||||
# one that lands in the seconds before this job runs.
|
||||
#
|
||||
# Approval stays narrow: only PRs the push-app-to-main app opens on a branch
|
||||
# of this repository (a script moving over from DevScripts, reviewed and
|
||||
# tested there) are approved when every script passes. Every other PR gets
|
||||
# the report, the check, and "changes requested" on a real failure.
|
||||
#
|
||||
# gate ubuntu-latest who asked, which scripts, which commit
|
||||
# test proxmox-test / build + checks, one matrix job per script, platform
|
||||
# incus-test / (pve, incus, arm) and OS; arm runs on GitHub's
|
||||
# ubuntu-24.04-arm arm64 runner and is information only
|
||||
# report ubuntu-latest check run + PR comment
|
||||
#
|
||||
# The test job gets a read-only token and never sees a secret: the host it runs
|
||||
# on executes untrusted code and has to be treated as compromised.
|
||||
#
|
||||
# This file is identical in DevScripts and ProxmoxVE; github.repository decides
|
||||
# which URLs get rewritten and which container ID range is used.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# Every PR, see above. pull_request_target runs this file from main, never
|
||||
# the PR's copy of it, and does not wait for "approve and run" on forks.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- "ct/**"
|
||||
- "install/**"
|
||||
- "vm/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: "Pull request number"
|
||||
required: true
|
||||
type: number
|
||||
sha:
|
||||
description: "Pin a commit (full 40-character SHA); empty = the latest, if nothing was pushed since"
|
||||
required: false
|
||||
type: string
|
||||
scripts:
|
||||
description: "Space-separated app names, ct/ or vm/ paths, and platforms (pve, incus, arm); empty = everything the PR touches, everywhere"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
# One queue per PR. A newer push replaces a run that is still waiting (its
|
||||
# gate would skip anyway: the head moved), but never kills one that is
|
||||
# building: a cancelled job leaves its guest behind until the sweeper. Any
|
||||
# comment starts this workflow; one that is not a /test gets a group of its
|
||||
# own, so it cannot push a waiting test out of the queue.
|
||||
concurrency:
|
||||
group: >-
|
||||
pr-script-test-${{ github.event_name == 'issue_comment' && !startsWith(github.event.comment.body, '/test')
|
||||
&& format('noop-{0}', github.run_id)
|
||||
|| github.event.pull_request.number || github.event.issue.number || inputs.pr }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/test')) ||
|
||||
(github.event_name == 'pull_request_target' && !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read # when the run was dispatched, for a /test without SHA
|
||||
checks: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
pr: ${{ steps.plan.outputs.pr }}
|
||||
repo: ${{ steps.plan.outputs.repo }}
|
||||
sha: ${{ steps.plan.outputs.sha }}
|
||||
check_id: ${{ steps.plan.outputs.check_id }}
|
||||
actor: ${{ steps.plan.outputs.actor }}
|
||||
not_built: ${{ steps.plan.outputs.not_built }}
|
||||
trusted: ${{ steps.plan.outputs.trusted }}
|
||||
kind: ${{ steps.plan.outputs.kind }}
|
||||
platforms: ${{ steps.plan.outputs.platforms }}
|
||||
base_sha: ${{ steps.plan.outputs.base_sha }}
|
||||
steps:
|
||||
- id: plan
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const MAX = 10;
|
||||
core.setOutput('matrix', '[]');
|
||||
|
||||
// Trusted: a PR the push-app-to-main app opened on a branch of this
|
||||
// repository, i.e. a script moving over from DevScripts.
|
||||
const TRUSTED_AUTHOR = 'push-app-to-main[bot]';
|
||||
const isTrusted = p => !!p && p.user && p.user.login === TRUSTED_AUTHOR && p.user.type === 'Bot' &&
|
||||
!!p.head.repo && p.head.repo.full_name === `${owner}/${repo}`;
|
||||
|
||||
let prNumber, requested = '';
|
||||
if (context.eventName === 'pull_request_target') {
|
||||
const p = context.payload.pull_request;
|
||||
prNumber = p.number;
|
||||
// Exactly the pushed commit; a later push starts its own run.
|
||||
requested = p.head.sha;
|
||||
} else if (context.eventName === 'workflow_dispatch') {
|
||||
prNumber = Number(context.payload.inputs.pr);
|
||||
requested = `${context.payload.inputs.sha || ''} ${context.payload.inputs.scripts || ''}`;
|
||||
} else {
|
||||
prNumber = context.payload.issue.number;
|
||||
const body = context.payload.comment.body.trim();
|
||||
if (!/^\/test(\s|$)/.test(body)) return;
|
||||
// author_association hides private org members, so ask for the permission itself.
|
||||
const user = context.payload.comment.user.login;
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: user });
|
||||
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
|
||||
core.notice(`${user} has ${perm.permission} access; /test needs write`);
|
||||
return;
|
||||
}
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner, repo, comment_id: context.payload.comment.id, content: 'rocket',
|
||||
});
|
||||
requested = body.split('\n')[0].slice('/test'.length);
|
||||
}
|
||||
|
||||
// `/test <app>` in an issue (not a PR) builds that app from the
|
||||
// default branch as it is right now, e.g. to see whether a reported
|
||||
// error reproduces. No PR, so no check run, no review, no approval.
|
||||
const isIssue = context.eventName === 'issue_comment' && !context.payload.issue.pull_request;
|
||||
let pr = null, head, headSha;
|
||||
if (isIssue) {
|
||||
const { data: info } = await github.rest.repos.get({ owner, repo });
|
||||
const { data: branch } = await github.rest.repos.getBranch({ owner, repo, branch: info.default_branch });
|
||||
headSha = branch.commit.sha;
|
||||
head = { owner: { login: owner }, name: repo, full_name: `${owner}/${repo}` };
|
||||
} else {
|
||||
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }));
|
||||
if (pr.state !== 'open' || !pr.head.repo) { core.notice('PR is closed or its fork is gone'); return; }
|
||||
head = pr.head.repo;
|
||||
headSha = pr.head.sha;
|
||||
if (context.eventName === 'pull_request_target' && headSha !== requested) {
|
||||
core.notice('Pushed to again since; the newer push has its own run');
|
||||
return;
|
||||
}
|
||||
}
|
||||
core.setOutput('kind', isIssue ? 'issue' : 'pr');
|
||||
core.setOutput('trusted', !isIssue && isTrusted(pr) ? 'true' : 'false');
|
||||
const MARK = '<!-- pr-script-test -->';
|
||||
const sticky = async body => {
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber });
|
||||
// Only our own comment: anyone can paste the marker into theirs.
|
||||
const mine = comments.find(c => c.body.includes(MARK) && c.user.type === 'Bot');
|
||||
if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
|
||||
else await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||
};
|
||||
|
||||
// Which commit the approval covers. Reading pr.head.sha alone would
|
||||
// build whatever was pushed between the comment and this job
|
||||
// starting, i.e. code nobody looked at. Two ways to be sure:
|
||||
//
|
||||
// /test <sha> that exact commit, and only while it is the head. A
|
||||
// full SHA: the author writes both commits, so a short
|
||||
// prefix is a quick birthday collision away.
|
||||
// /test the latest commit, if nothing reached the fork after
|
||||
// the comment. pushed_at is set by GitHub on every push
|
||||
// and read after the PR, so a push in between shows up.
|
||||
// Branches of this repository belong to people with
|
||||
// write access, who could build anything anyway.
|
||||
const words = requested.split(/\s+/).filter(Boolean);
|
||||
// Platform words pick where to build: `/test arm`, `/test pve incus
|
||||
// bark-server`. None means every platform. A run on only some of
|
||||
// them is reported under its own check name and never approves.
|
||||
const PLATFORM = { pve: 'pve', proxmox: 'pve', incus: 'incus', arm: 'arm', arm64: 'arm', aarch64: 'arm' };
|
||||
const platformOf = t => PLATFORM[t.toLowerCase()];
|
||||
const only = new Set(words.map(platformOf).filter(Boolean));
|
||||
const onlyList = ['pve', 'incus', 'arm'].filter(p => only.has(p));
|
||||
const tokens = words.filter(t => !platformOf(t));
|
||||
const isSha = t => /^[0-9a-f]{7,40}$/.test(t);
|
||||
// App names (bark-server) become ct/ paths; such a name without a
|
||||
// ct/ script falls back to vm/ below.
|
||||
const bare = new Set();
|
||||
const toPath = t => {
|
||||
if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(t) || !/^[A-Za-z0-9._-]+$/.test(t)) return t;
|
||||
const p = `ct/${t.replace(/\.sh$/, '')}.sh`;
|
||||
bare.add(p);
|
||||
return p;
|
||||
};
|
||||
const approved = isIssue ? [] : tokens.filter(isSha);
|
||||
let entries = (isIssue ? tokens : tokens.filter(t => !isSha(t))).map(toPath);
|
||||
const refuse = async (why, retry) => {
|
||||
await sticky([MARK, '### Proxmox script test', '', `⚠️ ${why} Nothing was built.`, '', retry].join('\n'));
|
||||
core.notice(why);
|
||||
};
|
||||
if (isIssue) {
|
||||
// The default branch is trusted; only which app is needed.
|
||||
if (entries.length === 0) {
|
||||
await refuse('In an issue, `/test` needs to know which app.', 'Comment `/test <app>`, for example `/test bark-server` (the name of its `ct/` script).');
|
||||
return;
|
||||
}
|
||||
} else if (approved.length > 1) {
|
||||
await refuse('`/test` takes at most one commit.', 'Comment `/test` to build the latest commit.');
|
||||
return;
|
||||
}
|
||||
if (approved.length === 1 && approved[0] !== headSha) {
|
||||
await refuse(
|
||||
approved[0].length !== 40 && headSha.startsWith(approved[0])
|
||||
? 'A short SHA is not enough, it has to be all 40 characters.'
|
||||
: `\`${approved[0]}\` is not the head of this PR (any more).`,
|
||||
'Comment `/test` to build the latest commit.');
|
||||
return;
|
||||
}
|
||||
if (!isIssue && approved.length === 0 && head.full_name !== `${owner}/${repo}`) {
|
||||
const askedAt = context.eventName === 'workflow_dispatch'
|
||||
? (await github.rest.actions.getWorkflowRun({ owner, repo, run_id: context.runId })).data.created_at
|
||||
: context.payload.comment.created_at;
|
||||
const { data: fork } = await github.rest.repos.get({ owner: head.owner.login, repo: head.name });
|
||||
if (new Date(fork.pushed_at) >= new Date(askedAt)) {
|
||||
await refuse(
|
||||
`Something was pushed to ${head.full_name} after the \`/test\`, so it is not certain which commit was meant.`,
|
||||
'Comment `/test` again once you have looked at the latest commit.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (entries.some(e => !/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(e))) {
|
||||
await refuse(
|
||||
isIssue ? 'Only app names (as in `ct/<app>.sh` or `vm/<name>.sh`) and platforms (`pve`, `incus`, `arm`) can be requested.' : 'Only app names, `ct/<name>.sh` and `vm/<name>.sh` paths, platforms (`pve`, `incus`, `arm`) and the commit can be requested.',
|
||||
isIssue ? 'For example: `/test bark-server`, `/test arm bark-server` or `/test debian-vm`' : 'For example: `/test`, `/test arm`, or `/test pve incus bark-server`.');
|
||||
return;
|
||||
}
|
||||
// Everything the PR changes that this test cannot build is named in
|
||||
// the comment, so a /test never ends in silence.
|
||||
const notBuilt = [];
|
||||
const code = s => '`' + String(s).replace(/[`\r\n]/g, '') + '`';
|
||||
if (entries.length === 0) {
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: prNumber });
|
||||
const set = new Set();
|
||||
for (const f of files) {
|
||||
if (f.status === 'removed') continue;
|
||||
let m;
|
||||
if ((m = f.filename.match(/^install\/([A-Za-z0-9._-]+)-install\.sh$/))) set.add(`ct/${m[1]}.sh`);
|
||||
else if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(f.filename)) set.add(f.filename);
|
||||
else if (/^(tools|turnkey)\/.*\.sh$/.test(f.filename)) notBuilt.push(`${code(f.filename)}: only \`ct/\` and \`vm/\` scripts are built`);
|
||||
}
|
||||
entries = [...set].sort();
|
||||
}
|
||||
|
||||
const include = [];
|
||||
// Upgrade test: install the ct/ script from the PR's base branch,
|
||||
// then update with the PR, as a user who installed before the merge
|
||||
// would. Only in ProxmoxVE, where users have installed it, and only
|
||||
// when the PR changes update_script(): with the same update code an
|
||||
// existing install updates exactly as it does today. New scripts
|
||||
// have nothing to upgrade from. Proxmox only, once per script.
|
||||
const UPGRADE_REPO = 'community-scripts/ProxmoxVE';
|
||||
let baseSha = '';
|
||||
if (!isIssue && `${owner}/${repo}` === UPGRADE_REPO) {
|
||||
const { data: b } = await github.rest.repos.getBranch({ owner, repo, branch: pr.base.ref });
|
||||
baseSha = b.commit.sha;
|
||||
}
|
||||
// From `function update_script() {` to the first `}` at the start of
|
||||
// a line; null without one.
|
||||
const updateCode = t => ((t || '').match(/^(function[ \t]+)?update_script[ \t]*\(\)[ \t]*\{[\s\S]*?^\}/m) || [null])[0];
|
||||
const updateChanged = async (p, headText) => {
|
||||
if (!baseSha) return false;
|
||||
let baseText;
|
||||
try {
|
||||
const res = await github.rest.repos.getContent({ owner, repo, path: p, ref: baseSha });
|
||||
baseText = Buffer.from(res.data.content || '', 'base64').toString('utf8');
|
||||
} catch (e) { return false; }
|
||||
const before = updateCode(baseText), after = updateCode(headText);
|
||||
return before !== null && after !== null && before !== after;
|
||||
};
|
||||
const fetchScript = async p => {
|
||||
try {
|
||||
const res = await github.rest.repos.getContent({ owner: head.owner.login, repo: head.name, path: p, ref: headSha });
|
||||
return Buffer.from(res.data.content || '', 'base64').toString('utf8');
|
||||
} catch (e) { return null; }
|
||||
};
|
||||
for (let script of entries.slice(0, MAX)) {
|
||||
let text = await fetchScript(script);
|
||||
// /test debian-vm in an issue: there is no ct/debian-vm.sh, but a vm/ one.
|
||||
const wasBare = isIssue || bare.has(script);
|
||||
if (text === null && wasBare && script.startsWith('ct/')) {
|
||||
const vm = `vm/${script.slice(3)}`;
|
||||
if ((text = await fetchScript(vm)) !== null) script = vm;
|
||||
}
|
||||
if (text === null) {
|
||||
const where = isIssue ? 'on the default branch' : 'in this PR';
|
||||
notBuilt.push(wasBare
|
||||
? (script.startsWith('ct/') ? `${code(script.slice(3, -3))}: neither \`ct/\` nor \`vm/\` has it ${where}` : `${code(script)} does not exist ${where}`)
|
||||
: `\`${script}\` does not exist in this PR (an \`install/\` script is built through the \`ct/\` script of the same name)`);
|
||||
continue;
|
||||
}
|
||||
const isVm = script.startsWith('vm/');
|
||||
// The test hosts are amd64; an ARM-only VM script refuses to run there.
|
||||
if (isVm && /^\s*vm_require_arch\s+arm64\b/m.test(text)) {
|
||||
notBuilt.push(`${code(script)}: ARM64 only, the test hosts are amd64`);
|
||||
continue;
|
||||
}
|
||||
// Scripts that ask for the OS get built once per OS.
|
||||
const oses = !isVm && /msg_menu\s+"Choose the container OS"/.test(text) ? ['debian', 'alpine'] : [''];
|
||||
const name = isVm ? `vm-${script.slice(3, -3)}` : script.slice(3, -3);
|
||||
// Every ct/ script on every platform: the same files run on Proxmox
|
||||
// and, through community-scripts/core's Incus backend, on Incus.
|
||||
// VMs are Proxmox only. ARM (Incus on GitHub's arm64 runner) is
|
||||
// information only and never decides the result; a script that
|
||||
// says var_arm64=no is not built there. Unset is built with
|
||||
// var_arm64=yes, to see whether it would work.
|
||||
const arm = (text.match(/^[ \t]*var_arm64="\$\{var_arm64:-(yes|no)\}"/m) || [])[1] || 'unset';
|
||||
let platforms = isVm ? ['pve'] : arm === 'no' ? ['pve', 'incus'] : ['pve', 'incus', 'arm'];
|
||||
if (!isVm && arm === 'no' && (!only.size || only.has('arm'))) notBuilt.push(`${code(script)} on ARM64: the script sets \`var_arm64=no\``);
|
||||
if (only.size) {
|
||||
if (isVm && !only.has('pve')) notBuilt.push(`${code(script)}: VM scripts are built on \`pve\` only`);
|
||||
platforms = platforms.filter(p => only.has(p));
|
||||
}
|
||||
for (const platform of platforms) {
|
||||
for (const os of oses) include.push({ script, os, platform, arm64: platform === 'arm' ? arm : '', mode: '', name: `${os ? `${name}-${os}` : name}-${platform}` });
|
||||
}
|
||||
if (!isVm && platforms.includes('pve') && await updateChanged(script, text)) {
|
||||
const os = oses[0];
|
||||
include.push({ script, os, platform: 'pve', arm64: '', mode: 'upgrade', name: `${os ? `${name}-${os}` : name}-pve-upgrade` });
|
||||
}
|
||||
}
|
||||
for (const script of entries.slice(MAX)) notBuilt.push(`\`${script}\`: at most ${MAX} scripts per run, name it with \`/test ${script}\``);
|
||||
if (include.length === 0) {
|
||||
await sticky([
|
||||
MARK, '### Proxmox script test', '',
|
||||
isIssue || onlyList.length
|
||||
? `ℹ️ Nothing to build${onlyList.length ? ` on ${onlyList.join(', ')}` : ''}:`
|
||||
: 'ℹ️ Nothing to build: this test builds `ct/` and `vm/` scripts (and changed `install/` scripts through their `ct/` script).', '',
|
||||
...(notBuilt.length ? notBuilt.map(n => `- ${n}`) : ['- this PR changes no `ct/`, `vm/` or `install/` script']),
|
||||
].join('\n'));
|
||||
core.notice('No ct/ or vm/ script to build');
|
||||
return;
|
||||
}
|
||||
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
// A check run belongs to a PR's commit; an issue has none.
|
||||
let checkId = '';
|
||||
if (!isIssue) {
|
||||
const { data: check } = await github.rest.checks.create({
|
||||
// A partial run gets its own name: it must not stand in for the
|
||||
// full test, e.g. as a required check.
|
||||
owner, repo, name: `Proxmox script test${onlyList.length ? ` (${onlyList.join(', ')} only)` : ''}`, head_sha: headSha, status: 'in_progress',
|
||||
details_url: runUrl,
|
||||
output: { title: `Building ${include.length}`, summary: include.map(i => `- \`${i.script}\` ${[i.platform, i.os, i.mode].filter(Boolean).join(' · ')}`).join('\n') },
|
||||
});
|
||||
checkId = String(check.id);
|
||||
}
|
||||
|
||||
core.setOutput('matrix', JSON.stringify(include));
|
||||
core.setOutput('pr', String(prNumber));
|
||||
core.setOutput('repo', head.full_name);
|
||||
core.setOutput('sha', headSha);
|
||||
core.setOutput('check_id', checkId);
|
||||
core.setOutput('not_built', JSON.stringify(notBuilt));
|
||||
core.setOutput('platforms', onlyList.join(' '));
|
||||
core.setOutput('base_sha', baseSha);
|
||||
// Who asked: the /test commenter, or whoever dispatched the run.
|
||||
core.setOutput('actor', context.actor);
|
||||
|
||||
await sticky([
|
||||
MARK, '### Proxmox script test', '',
|
||||
`⏳ Building ${include.length} at ${headSha.slice(0, 7)}${isIssue ? ' (default branch)' : ''}${onlyList.length ? `, only on ${onlyList.join(', ')}` : ''} — [watch it live](${runUrl}) (open the \`test\` job)`, '',
|
||||
...include.map(i => `- \`${i.script}\` (${[i.platform, i.os, i.mode && 'upgrade from the base branch'].filter(Boolean).join(' · ')})`),
|
||||
...(notBuilt.length ? ['', '**Not built:**', ...notBuilt.map(n => `- ${n}`)] : []),
|
||||
].join('\n'));
|
||||
|
||||
test:
|
||||
needs: gate
|
||||
if: needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
|
||||
# Only `proxmox-test` / `incus-test`, never `self-hosted`: the runners are
|
||||
# registered without default labels so that no other workflow can be
|
||||
# scheduled onto them. ARM runs on GitHub's own arm64 runner, a fresh VM
|
||||
# per job (free for public repositories).
|
||||
runs-on: ${{ matrix.platform == 'arm' && 'ubuntu-24.04-arm' || matrix.platform == 'incus' && 'incus-test' || 'proxmox-test' }}
|
||||
# ARM is information only: a failed or missing ARM job (no runner, no
|
||||
# minutes left, ...) never turns the run red. The report leaves it out of
|
||||
# the result as well.
|
||||
continue-on-error: ${{ matrix.platform == 'arm' }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 120 # install (45) + update (30) + second update + checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.gate.outputs.matrix) }}
|
||||
name: test (${{ matrix.name }})
|
||||
steps:
|
||||
# The default branch, never the PR: the harness itself must be trusted.
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .github/workflows/scripts
|
||||
persist-credentials: false
|
||||
|
||||
# The arm64 runner is a plain Ubuntu VM: Incus and the screenshot
|
||||
# browser are set up on it for this one job.
|
||||
- name: Set up the ARM runner
|
||||
if: matrix.platform == 'arm'
|
||||
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
|
||||
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
|
||||
|
||||
- name: Build and check ${{ matrix.script }}
|
||||
env:
|
||||
PR_REPO: ${{ needs.gate.outputs.repo }}
|
||||
PR_SHA: ${{ needs.gate.outputs.sha }}
|
||||
SCRIPT: ${{ matrix.script }}
|
||||
OS: ${{ matrix.os }}
|
||||
# ARM is the Incus harness on another machine.
|
||||
PLATFORM: ${{ matrix.platform == 'arm' && 'incus' || matrix.platform }}
|
||||
VAR_ARM64: ${{ matrix.platform == 'arm' && 'yes' || '' }}
|
||||
# upgrade: install from the base branch, update with the PR.
|
||||
TEST_MODE: ${{ matrix.mode || '' }}
|
||||
BASE_SHA: ${{ needs.gate.outputs.base_sha }}
|
||||
UPSTREAM: ${{ github.repository }}
|
||||
CTID_BASE: ${{ github.repository == 'community-scripts/ProxmoxVE' && '9050' || '9000' }}
|
||||
# Only so release lookups are not rate limited: unauthenticated, all
|
||||
# test containers share the lab's one public IP and 60 calls an hour.
|
||||
# This job's token can read public contents and expires with the job.
|
||||
GH_API_TOKEN: ${{ github.token }}
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
# The self-hosted runners run as root; the GitHub one needs sudo (pct
|
||||
# and incus do).
|
||||
run: >-
|
||||
${{ matrix.platform == 'arm' && 'sudo --preserve-env=PR_REPO,PR_SHA,SCRIPT,OS,PLATFORM,VAR_ARM64,TEST_MODE,BASE_SHA,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT' || '' }}
|
||||
bash .github/workflows/scripts/pve-run-script.sh
|
||||
|
||||
- if: always() && matrix.platform == 'arm'
|
||||
run: sudo chmod -R a+rX "$RUNNER_TEMP/result" 2>/dev/null || true
|
||||
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: result-${{ matrix.name }}
|
||||
path: ${{ runner.temp }}/result
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
report:
|
||||
needs: [gate, test]
|
||||
if: always() && needs.gate.result == 'success' && needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
checks: write
|
||||
contents: write # the ci-screenshots branch only; main is protected
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: result-*
|
||||
path: results
|
||||
|
||||
- uses: actions/github-script@v8
|
||||
env:
|
||||
MATRIX: ${{ needs.gate.outputs.matrix }}
|
||||
PR: ${{ needs.gate.outputs.pr }}
|
||||
SHA: ${{ needs.gate.outputs.sha }}
|
||||
CHECK_ID: ${{ needs.gate.outputs.check_id }}
|
||||
TEST_RESULT: ${{ needs.test.result }}
|
||||
ACTOR: ${{ needs.gate.outputs.actor }}
|
||||
NOT_BUILT: ${{ needs.gate.outputs.not_built }}
|
||||
TRUSTED: ${{ needs.gate.outputs.trusted }}
|
||||
KIND: ${{ needs.gate.outputs.kind }}
|
||||
PLATFORMS: ${{ needs.gate.outputs.platforms }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const { owner, repo } = context.repo;
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
const matrix = JSON.parse(process.env.MATRIX);
|
||||
|
||||
// download-artifact unpacks a single artifact straight into results/
|
||||
// and several into results/<name>/, so look for every result.json
|
||||
// and match it by what it says it is.
|
||||
const path = require('path');
|
||||
const found = new Map();
|
||||
const walk = dir => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p);
|
||||
else if (e.name === 'result.json') {
|
||||
try {
|
||||
const r = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
// Written on the host that ran the PR's code: anything but a
|
||||
// known status is an error, never something new.
|
||||
if (!['pass', 'fail', 'unsupported', 'error'].includes(r.status)) r.status = 'error';
|
||||
const log = path.join(dir, 'install.log');
|
||||
r.install_log = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
|
||||
const shot = path.join(dir, 'screenshot.png');
|
||||
r.shot_file = fs.existsSync(shot) ? shot : '';
|
||||
if (!['updated', 'no-update', 'fail', 'error', ''].includes(r.update_status || '')) r.update_status = '';
|
||||
// Installed fine, but the update could not be tested (e.g. a
|
||||
// GitHub rate limit): not a pass, and not the PR's fault.
|
||||
if (r.status === 'pass' && r.update_status === 'error') { r.status = 'error'; r.step = `update ${r.update_step}`; }
|
||||
const ulog = path.join(dir, 'update.log');
|
||||
r.update_log = fs.existsSync(ulog) ? fs.readFileSync(ulog, 'utf8') : '';
|
||||
const u2log = path.join(dir, 'update2.log');
|
||||
r.update2_log = fs.existsSync(u2log) ? fs.readFileSync(u2log, 'utf8') : '';
|
||||
if (!['upgrade', ''].includes(r.mode || '')) r.mode = '';
|
||||
r.mode = r.mode || '';
|
||||
r.notes = Array.isArray(r.notes) ? r.notes.filter(n => typeof n === 'string').slice(0, 8) : [];
|
||||
r.resources = typeof r.resources === 'string' ? r.resources : '';
|
||||
const ushot = path.join(dir, 'screenshot-update.png');
|
||||
r.shot_update_file = fs.existsSync(ushot) ? ushot : '';
|
||||
// ARM runs the Incus harness on an arm64 machine; result.json
|
||||
// says so in arch. Only the platforms this workflow knows: an
|
||||
// unknown one would never match a matrix row anyway.
|
||||
if (r.arch === 'aarch64') r.platform = 'arm';
|
||||
else if (!['pve', 'incus'].includes(r.platform)) r.platform = 'pve';
|
||||
found.set(`${r.script}|${r.platform}|${r.os}|${r.mode}`, r);
|
||||
} catch (err) { core.warning(`unreadable ${p}: ${err.message}`); }
|
||||
}
|
||||
}
|
||||
};
|
||||
walk('results');
|
||||
|
||||
// A job that died before writing result.json still gets a row.
|
||||
const rows = matrix.map(m => ({ ...(found.get(`${m.script}|${m.platform || 'pve'}|${m.os || 'default'}|${m.mode || ''}`) || {
|
||||
script: m.script, os: m.os || 'default', platform: m.platform || 'pve', mode: m.mode || '', notes: [], resources: '', update2_log: '',
|
||||
status: process.env.TEST_RESULT === 'cancelled' ? 'cancelled' : 'error',
|
||||
step: 'no result (runner or job failure)', duration: 0, log_tail: '', install_log: '', shot_file: '',
|
||||
update_status: '', update_log: '', shot_update_file: '',
|
||||
}), name: m.name }));
|
||||
// Where a row ran: "pve", "incus · alpine", "arm64", "pve · upgrade", or "VM" (Proxmox only).
|
||||
const isVm = r => r.script.startsWith('vm/');
|
||||
const isArm = r => r.platform === 'arm';
|
||||
const where = r => {
|
||||
if (isVm(r)) return 'VM';
|
||||
const p = isArm(r) ? 'arm64' : r.platform;
|
||||
const w = r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
|
||||
return r.mode === 'upgrade' ? `${w} · upgrade` : w;
|
||||
};
|
||||
// ARM is information only: shown, never counted. The result, the
|
||||
// check, the review and the approval come from Proxmox and Incus.
|
||||
const coreRows = rows.filter(r => !isArm(r));
|
||||
const armRows = rows.filter(isArm);
|
||||
|
||||
// Screenshots go to an orphan branch, because a comment can only show
|
||||
// an image that has a URL. One file per PR and script, overwritten on
|
||||
// every run; the link names the commit, so no cache shows an old one.
|
||||
// The PNG comes from the host that ran the PR's code: check it again.
|
||||
const BRANCH = 'ci-screenshots';
|
||||
const prNum = Number(process.env.PR);
|
||||
const isPng = buf => buf.length > 8 && buf.length < 3000000 &&
|
||||
buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
const ensureBranch = async () => {
|
||||
try { await github.rest.git.getRef({ owner, repo, ref: `heads/${BRANCH}` }); return; }
|
||||
catch (e) { if (e.status !== 404) throw e; }
|
||||
const { data: blob } = await github.rest.git.createBlob({ owner, repo, encoding: 'utf-8',
|
||||
content: 'Screenshots taken by pr-script-test.yml for the PR comments. Not code; never merge this branch.\n' });
|
||||
const { data: tree } = await github.rest.git.createTree({ owner, repo, tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: blob.sha }] });
|
||||
const { data: commit } = await github.rest.git.createCommit({ owner, repo, message: 'Start ci-screenshots', tree: tree.sha, parents: [] });
|
||||
await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${BRANCH}`, sha: commit.sha });
|
||||
};
|
||||
const uploadShot = async (r, local, suffix) => {
|
||||
const buf = fs.readFileSync(local);
|
||||
if (!isPng(buf)) { core.warning(`${r.script}: screenshot is not a plausible PNG, skipped`); return ''; }
|
||||
const file = `${process.env.KIND === 'issue' ? 'issue' : 'pr'}-${prNum}/${r.name}${suffix}.png`;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
await ensureBranch();
|
||||
let sha;
|
||||
try { sha = (await github.rest.repos.getContent({ owner, repo, path: file, ref: BRANCH })).data.sha; }
|
||||
catch (e) { if (e.status !== 404) throw e; }
|
||||
const { data } = await github.rest.repos.createOrUpdateFileContents({
|
||||
owner, repo, branch: BRANCH, path: file, sha, content: buf.toString('base64'),
|
||||
message: `Screenshot: ${r.script} (${where(r)})${suffix} for #${prNum} at ${process.env.SHA.slice(0, 7)}`,
|
||||
});
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${data.commit.sha}/${file}`;
|
||||
} catch (e) {
|
||||
// Two reports at once race on the branch head; try again.
|
||||
core.warning(`${r.script}: screenshot upload attempt ${attempt} failed: ${e.message}`);
|
||||
await new Promise(res => setTimeout(res, 2000 * attempt));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
for (const r of rows) {
|
||||
if (r.shot_file) r.shot_url = await uploadShot(r, r.shot_file, '');
|
||||
if (r.shot_update_file) r.shot_update_url = await uploadShot(r, r.shot_update_file, '-after-update');
|
||||
}
|
||||
|
||||
// unsupported: the script needs a CPU feature the test host lacks
|
||||
// (AVX2 and the like). Not the PR's fault, so not a failure.
|
||||
const icon = { pass: '✅', fail: '❌', error: '⚠️', cancelled: '⏹️', unsupported: '🖥️' };
|
||||
const dur = s => `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`;
|
||||
// Text from the build lands outside code blocks here: no table breaks,
|
||||
// no code spans, and no @mention of a stranger (zero-width space).
|
||||
const clean = s => String(s || '').replace(/[|\r\n]/g, ' ').replace(/`/g, "'").replace(/@/g, '@').slice(0, 200);
|
||||
const fence = s => String(s || '').replace(/~~~~/g, '~~~');
|
||||
|
||||
// fail: the app broke, the PR's business. error/cancelled: our test
|
||||
// setup did, not the PR's. unsupported: the host's CPU is too old.
|
||||
const count = s => coreRows.filter(r => r.status === s).length;
|
||||
const passed = count('pass');
|
||||
const untestable = count('unsupported');
|
||||
const broken = count('fail');
|
||||
const setupTrouble = count('error') + count('cancelled');
|
||||
const allPass = passed === coreRows.length;
|
||||
// `/test arm`: nothing that counts was built, so the result is
|
||||
// information only as a whole.
|
||||
const armOnly = coreRows.length === 0;
|
||||
// Built on only some platforms (`/test pve`): a pass there says
|
||||
// nothing about the others, so it neither approves nor lifts an
|
||||
// earlier "changes requested". A failure is still a failure.
|
||||
const partial = !!process.env.PLATFORMS;
|
||||
const cancelled = (armOnly ? rows : coreRows).every(r => r.status === 'cancelled');
|
||||
const of = `**${passed}/${coreRows.length} passed**`;
|
||||
const armPassed = armRows.filter(r => r.status === 'pass').length;
|
||||
const armLine = armRows.length && !armOnly
|
||||
? `ARM64 (information only, not counted): ${armPassed}/${armRows.length} passed${armPassed < armRows.length ? `; failures there do not block this PR` : ''}.`
|
||||
: '';
|
||||
const headline = armOnly
|
||||
? `ℹ️ ARM64 only, information only: **${armPassed}/${armRows.length} passed**`
|
||||
: broken
|
||||
? `❌ ${of}`
|
||||
: setupTrouble
|
||||
? `⚠️ ${of}, ${setupTrouble} could not be tested because of a problem with the test setup (not the PR)`
|
||||
: untestable
|
||||
? `🖥️ ${of}, ${untestable} need${untestable === 1 ? 's' : ''} a newer CPU than the test host has (nothing failed)`
|
||||
: `✅ ${of}`;
|
||||
|
||||
// Update: the installed app is set back a version and updated with
|
||||
// the PR's update_script. ➖ = it ran but had nothing to update.
|
||||
const updateIcon = { updated: '✅', 'no-update': '➖', fail: '❌', error: '⚠️' };
|
||||
const detail = r => {
|
||||
if (r.status !== 'pass') return clean(r.step);
|
||||
const ip = /^[0-9.]{7,15}$/.test(r.ip || '') ? r.ip : '';
|
||||
if (isVm(r) && !r.url) return ip ? `booted, got ${ip}` : 'booted, no DHCP lease (an installer ISO, or no network)';
|
||||
const reach = r.url ? `${clean(r.url)} → ${r.http_code}` : 'no URL to check';
|
||||
return r.update_status === 'no-update' ? `${reach}; update: nothing to update${r.update_step ? ` (${clean(r.update_step)})` : ''}` : reach;
|
||||
};
|
||||
const table = [
|
||||
'| Script | Platform | Install | Update | Duration | Detail |',
|
||||
'|---|---|---|---|---|---|',
|
||||
...rows.map(r => `| \`${r.script}\` | ${where(r)} | ${r.update_status ? '✅' : icon[r.status] || '❔'} | ${updateIcon[r.update_status] || '–'} | ${dur(r.duration || 0)} | ${detail(r)} |`),
|
||||
].join('\n');
|
||||
|
||||
// A comment holds 65536 characters. Each script gets an equal share
|
||||
// and a log that does not fit loses its beginning: the end is where
|
||||
// a failure shows.
|
||||
const budget = Math.floor(52000 / rows.length);
|
||||
const tail = (text, max) => {
|
||||
text = fence(text).trimEnd();
|
||||
if (text.length <= max) return text;
|
||||
const kept = text.slice(text.length - max);
|
||||
const cut = text.slice(0, text.length - max).split('\n').length;
|
||||
return `… ${cut} earlier lines cut, the full log is in the run artifacts\n${kept.slice(kept.indexOf('\n') + 1)}`;
|
||||
};
|
||||
const details = (summaryLine, text, max) => [
|
||||
`<details><summary>${summaryLine}</summary>`, '',
|
||||
'~~~~text', tail(text, max), '~~~~', '</details>',
|
||||
].join('\n');
|
||||
|
||||
const logs = rows.map(r => {
|
||||
const parts = [];
|
||||
const failed = r.status !== 'pass';
|
||||
// A VM script's own output is all there is: no install log inside.
|
||||
const buildShare = (failed || isVm(r)) && r.log_tail ? Math.min(r.log_tail.length, Math.floor(budget / 3)) : 0;
|
||||
if (buildShare) parts.push(details(`${icon[r.status] || '❔'} ${r.script} (${where(r)}) — build log`, r.log_tail, buildShare));
|
||||
if (isVm(r)) return parts.join('\n\n');
|
||||
// The update log gets up to a third of what is left, the install log the rest.
|
||||
const updateShare = r.update_log ? Math.min(r.update_log.length, Math.floor((budget - buildShare) / 3)) : 0;
|
||||
if (r.install_log) {
|
||||
const lines = r.install_log.trimEnd().split('\n').length;
|
||||
parts.push(details(`📄 ${r.script} (${where(r)}) — install log (${lines} lines)`, r.install_log, budget - buildShare - updateShare - 400));
|
||||
} else if (!buildShare) {
|
||||
parts.push(`<sub>${r.script} (${where(r)}): no install log was captured</sub>`);
|
||||
}
|
||||
if (updateShare) {
|
||||
const lines = r.update_log.trimEnd().split('\n').length;
|
||||
parts.push(details(`🔄 ${r.script} (${where(r)}) — update log (${lines} lines)`, r.update_log, updateShare));
|
||||
}
|
||||
if (r.update2_log) {
|
||||
parts.push(details(`🔁 ${r.script} (${where(r)}) — second update log`, r.update2_log, Math.min(r.update2_log.length, Math.floor(budget / 8))));
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
});
|
||||
|
||||
const shots = rows.filter(r => r.shot_url || r.shot_update_url).map(r => [
|
||||
`<details open><summary>📸 ${r.script} (${where(r)}) — ${r.url ? clean(r.url) : 'console'}${r.shot_url && r.shot_update_url ? ': after install / after update' : r.shot_update_url ? ': after update' : ''}</summary>`, '',
|
||||
[r.shot_url, r.shot_update_url].filter(Boolean)
|
||||
.map((u, i, all) => `<img src="${u}" width="${all.length > 1 ? 420 : 640}" alt="Screenshot of ${r.script}">`).join(' '),
|
||||
'', '</details>',
|
||||
].join('\n'));
|
||||
|
||||
const summary = [
|
||||
`${headline} at \`${process.env.SHA.slice(0, 7)}\``, '',
|
||||
table, '',
|
||||
...(armLine ? [`<sub>${armLine}</sub>`, ''] : []),
|
||||
...(rows.some(r => r.mode === 'upgrade')
|
||||
? ['<sub>`upgrade`: installed from the base branch as it is now, then updated with this PR, as a user who installed before the merge would.</sub>', '']
|
||||
: []),
|
||||
...(() => {
|
||||
const noted = rows.filter(r => r.notes.length);
|
||||
return noted.length
|
||||
? ['**Notes** (information only, not counted):', ...noted.flatMap(r => r.notes.map(n => `- \`${r.script}\` (${where(r)}): ${clean(n)}`)), '']
|
||||
: [];
|
||||
})(),
|
||||
...(() => {
|
||||
const measured = rows.filter(r => r.resources);
|
||||
return measured.length
|
||||
? ['<details><summary>📊 Resources</summary>', '', '| Script | Platform | Usage |', '|---|---|---|',
|
||||
...measured.map(r => `| \`${r.script}\` | ${where(r)} | ${clean(r.resources)} |`), '', '</details>', '']
|
||||
: [];
|
||||
})(),
|
||||
...(partial ? [`<sub>Built only on ${process.env.PLATFORMS.split(' ').join(', ')}; comment \`/test\` for every platform.</sub>`, ''] : []),
|
||||
...shots, '',
|
||||
...(() => { try { const n = JSON.parse(process.env.NOT_BUILT || '[]'); return n.length ? ['**Not built:**', ...n.map(x => `- ${x}`), ''] : []; } catch (e) { return []; } })(),
|
||||
...logs, '',
|
||||
`Full logs: [workflow run](${runUrl}) (artifacts \`result-*\`). Comment \`/test${partial ? ` ${process.env.PLATFORMS}` : ''}${process.env.KIND === 'issue' ? ' ' + [...new Set(rows.map(r => r.script.slice(3, -3)))].join(' ') : ''}\` to build again.`,
|
||||
].join('\n');
|
||||
|
||||
// Issues have no check run (there is no PR commit to attach it to).
|
||||
if (process.env.CHECK_ID) {
|
||||
await github.rest.checks.update({
|
||||
owner, repo, check_run_id: Number(process.env.CHECK_ID), status: 'completed',
|
||||
// Untestable here is "test it by hand", not a pass: action_required,
|
||||
// because a required check would count neutral as passed.
|
||||
conclusion: cancelled ? 'cancelled' : armOnly ? 'neutral' : allPass ? 'success' : broken ? 'failure' : 'action_required',
|
||||
output: {
|
||||
title: armOnly
|
||||
? `ARM64 ${armPassed}/${armRows.length} passed (information only)`
|
||||
: `${passed}/${coreRows.length} passed${armRows.length ? ` (+ ARM ${armPassed}/${armRows.length}, not counted)` : ''}`,
|
||||
summary: summary.slice(0, 65000),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The result pings whoever started the test. A mention added by
|
||||
// editing a comment does not reliably notify, so the old comment
|
||||
// goes and the result is posted as a new one, at the end of the
|
||||
// thread where it is looked for.
|
||||
// Whoever started the test and the PR's author, once each; no bots.
|
||||
const pr = Number(process.env.PR);
|
||||
const isIssue = process.env.KIND === 'issue';
|
||||
const { data: prInfo } = isIssue
|
||||
? await github.rest.issues.get({ owner, repo, issue_number: pr })
|
||||
: await github.rest.pulls.get({ owner, repo, pull_number: pr });
|
||||
const author = prInfo.user && prInfo.user.type === 'User' ? prInfo.user.login : '';
|
||||
const login = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
||||
const ping = [...new Set([process.env.ACTOR || '', author])].filter(l => login.test(l)).map(l => `@${l} `).join('');
|
||||
const MARK = '<!-- pr-script-test -->';
|
||||
const body = `${MARK}\n### Proxmox script test\n\n${ping}${summary}`.slice(0, 65000);
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr });
|
||||
for (const c of comments.filter(c => c.body.includes(MARK) && c.user.type === 'Bot')) {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id });
|
||||
}
|
||||
const { data: posted } = await github.rest.issues.createComment({ owner, repo, issue_number: pr, body });
|
||||
// An issue has nothing to review or approve: the comment is all.
|
||||
if (isIssue) return;
|
||||
|
||||
// A real failure requests changes on the tested commit. Untestable
|
||||
// (CPU), runner errors and cancelled runs are not the PR's fault and
|
||||
// leave the review state alone. Our previous "changes requested" is
|
||||
// always dismissed first: a pass lifts it, a new failure replaces it.
|
||||
const REVIEW_MARK = '<!-- pr-script-test-review -->';
|
||||
const failedRows = cancelled ? [] : coreRows.filter(r => r.status === 'fail');
|
||||
const sha7 = process.env.SHA.slice(0, 7);
|
||||
// Trusted PRs (scripts moving over from DevScripts) are approved when
|
||||
// every script passes -- not on untestable or setup trouble, which
|
||||
// means "not tested", not "good". That approval counts toward the
|
||||
// ruleset's required review; a push dismisses it as stale, and a
|
||||
// later run that does not fully pass withdraws it.
|
||||
// Anyone with write access can push to a migration branch, and the
|
||||
// approval counts toward the required review. So it only covers
|
||||
// what a migration contains: ct/ and install/ scripts. A PR that
|
||||
// also changes anything else (workflows, misc/, ...) is tested but
|
||||
// never approved.
|
||||
let approve = process.env.TRUSTED === 'true' && allPass && !partial;
|
||||
// A partial run that passed leaves every review as it is.
|
||||
const leaveReviews = partial && !failedRows.length;
|
||||
let noApproveWhy = '';
|
||||
if (approve) {
|
||||
const changed = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr });
|
||||
const other = changed.map(f => f.filename).filter(n => !/^(ct|install)\/[A-Za-z0-9._-]+\.sh$/.test(n));
|
||||
if (other.length) {
|
||||
approve = false;
|
||||
noApproveWhy = `Not approved automatically: besides \`ct/\` and \`install/\` scripts this PR changes ${other.slice(0, 5).map(n => '`' + n.replace(/[`\r\n]/g, '') + '`').join(', ')}${other.length > 5 ? ', …' : ''}. That needs a human review.`;
|
||||
}
|
||||
}
|
||||
if (noApproveWhy) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: posted.id, body: `${body}\n\n> ${noApproveWhy}`.slice(0, 65000) });
|
||||
}
|
||||
try {
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr });
|
||||
const ours = state => reviews.filter(v => v.user && v.user.type === 'Bot' && v.state === state && (v.body || '').includes(REVIEW_MARK));
|
||||
for (const v of leaveReviews ? [] : ours('CHANGES_REQUESTED')) {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner, repo, pull_number: pr, review_id: v.id,
|
||||
message: failedRows.length ? 'Replaced by a newer Proxmox script test.' : `Proxmox script test passed at ${sha7}.`,
|
||||
});
|
||||
}
|
||||
if (!approve && !cancelled && !leaveReviews) {
|
||||
for (const v of ours('APPROVED')) {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner, repo, pull_number: pr, review_id: v.id,
|
||||
message: `Withdrawn: the Proxmox script test at ${sha7} did not fully pass.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (approve && !ours('APPROVED').some(v => v.commit_id === process.env.SHA)) {
|
||||
await github.rest.pulls.createReview({
|
||||
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'APPROVE',
|
||||
body: [
|
||||
REVIEW_MARK, `**Proxmox script test passed** at \`${sha7}\`: ${rows.map(r => `\`${r.script}\``).join(', ')}.`, '',
|
||||
`Script moved over from DevScripts, built and checked on a real Proxmox host. [Test result](${posted.html_url}).`,
|
||||
].join('\n'),
|
||||
});
|
||||
}
|
||||
if (failedRows.length) {
|
||||
await github.rest.pulls.createReview({
|
||||
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'REQUEST_CHANGES',
|
||||
body: [
|
||||
REVIEW_MARK, `**Proxmox script test failed** at \`${sha7}\`:`, '',
|
||||
...failedRows.map(r => `- \`${r.script}\` (${where(r)}): ${clean(r.step)}`), '',
|
||||
`Logs and details: [test result](${posted.html_url}). Push a fix, then ask for \`/test\` again.`,
|
||||
].join('\n'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not update the review state: ${e.message}`);
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turns a GitHub-hosted arm64 runner (ubuntu-24.04-arm) into a throwaway Incus
|
||||
# test host for pve-run-script.sh (PLATFORM=incus): the same layout as
|
||||
# incus-ci-host-setup.sh, minus what a VM that lives for one job does not need
|
||||
# (services, sweeper, runner registration). Run with sudo.
|
||||
#
|
||||
# The runner is a fresh VM per job and is discarded afterwards; nothing here
|
||||
# has to be undone.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NET_ADDR=10.98.0.1
|
||||
NET_CIDR=10.98.0.0/24
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
start=$(date +%s)
|
||||
|
||||
# --- Incus ----------------------------------------------------------------------
|
||||
# Ubuntu 24.04's own incus is 6.0.0, which lacks `-f csv,noheader`; the engine
|
||||
# then finds no storage pool. Zabbly (the Incus maintainers' packages) has the
|
||||
# current 6.0 LTS, as Debian 13 does.
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
|
||||
# The key Zabbly publishes in its install instructions, not whatever the
|
||||
# server returns today.
|
||||
gpg --show-keys --with-colons /etc/apt/keyrings/zabbly.asc 2>/dev/null |
|
||||
awk -F: '/^fpr/ {print $10; exit}' | grep -qx 4EFC590696CB15B87C73A3AD82CC8797C838DCFD ||
|
||||
{ echo "::error::unexpected Zabbly signing key"; exit 1; }
|
||||
# Only the Incus packages may come from there; everything else stays Ubuntu's.
|
||||
printf '%s\n' 'Package: *' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 1' '' \
|
||||
'Package: incus* liblxc* lxcfs* qemu* edk2*' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 600' \
|
||||
>/etc/apt/preferences.d/zabbly
|
||||
printf '%s\n' 'Enabled: yes' 'Types: deb' 'URIs: https://pkgs.zabbly.com/incus/lts-6.0' \
|
||||
"Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")" 'Components: main' \
|
||||
"Architectures: $(dpkg --print-architecture)" 'Signed-By: /etc/apt/keyrings/zabbly.asc' \
|
||||
>/etc/apt/sources.list.d/zabbly-incus-lts-6.0.sources
|
||||
# Each slow step has its own limit, so a hang names itself in the log instead
|
||||
# of holding the PR report until the job times out.
|
||||
step() { local t=$1; shift; timeout "$t" "$@" || { echo "::error::'$*' failed or hung (limit ${t})"; return 1; }; }
|
||||
# What the runner's network looks like when apt can't get through.
|
||||
netdiag() {
|
||||
echo "::group::Network on this runner"
|
||||
resolvectl status 2>&1 | grep -E 'DNS Servers|Current DNS|resolv.conf mode' || cat /etc/resolv.conf
|
||||
local h
|
||||
for h in ports.ubuntu.com pkgs.zabbly.com github.com; do
|
||||
echo "$h -> $(timeout 10 getent ahostsv4 "$h" | awk '{print $1; exit}')"
|
||||
timeout 15 curl -s -o /dev/null -w " http %{http_code} in %{time_total}s\n" "http://$h/" || echo " no answer"
|
||||
done
|
||||
echo "::endgroup::"
|
||||
}
|
||||
# The runner's resolver (Azure's 168.63.129.16) stopped resolving the arm64
|
||||
# mirror on 2026-11-11 while every other name worked. Then the name is
|
||||
# looked up over DNS-over-HTTPS and pinned in /etc/hosts for this job.
|
||||
for h in ports.ubuntu.com; do
|
||||
timeout 10 getent ahostsv4 "$h" >/dev/null && continue
|
||||
ip=$(curl -fsS --max-time 10 -H 'accept: application/dns-json' "https://1.1.1.1/dns-query?name=$h&type=A" |
|
||||
jq -r '[.Answer[]? | select(.type == 1) | .data][0] // empty')
|
||||
if [[ "$ip" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
|
||||
echo "$ip $h" >>/etc/hosts
|
||||
echo "::warning::the runner's DNS can't resolve $h; using $ip from 1.1.1.1"
|
||||
fi
|
||||
done
|
||||
step 5m apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 update -qq || { netdiag; exit 1; }
|
||||
DEBIAN_FRONTEND=noninteractive step 10m apt-get install -y -qq incus jq >/dev/null
|
||||
|
||||
# Docker on the runner sets the FORWARD policy to DROP, which cuts the Incus
|
||||
# containers off from the internet.
|
||||
iptables -P FORWARD ACCEPT
|
||||
|
||||
step 2m incus admin init --preseed <<EOF
|
||||
networks:
|
||||
- name: incusbr0
|
||||
type: bridge
|
||||
config:
|
||||
ipv4.address: ${NET_ADDR}/24
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: none
|
||||
storage_pools:
|
||||
- name: default
|
||||
driver: dir
|
||||
profiles:
|
||||
- name: default
|
||||
devices:
|
||||
root:
|
||||
path: /
|
||||
pool: default
|
||||
type: disk
|
||||
eth0:
|
||||
name: eth0
|
||||
network: incusbr0
|
||||
type: nic
|
||||
EOF
|
||||
|
||||
# The PR tree server the harness expects (a service on the lab hosts).
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
nohup python3 -m http.server "$SERVE_PORT" --bind "$NET_ADDR" --directory "$SERVE_ROOT" >/tmp/ci-serve.log 2>&1 &
|
||||
echo "Incus $(incus --version) ready after $(($(date +%s) - start))s"
|
||||
|
||||
# --- Screenshots ----------------------------------------------------------------
|
||||
# Optional: without them the build is still tested. The harness takes one only
|
||||
# when `chromium` exists, the user ci-shot exists and the CI_SHOT chain keeps
|
||||
# that user off private addresses (here: the cloud metadata service at
|
||||
# 169.254.169.254 and the runner itself).
|
||||
shots() {
|
||||
useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
# Ubuntu's apt chromium is a snap wrapper; Playwright ships a plain arm64
|
||||
# build. Started directly with --screenshot, that build crashes on this
|
||||
# runner (sandbox or not), so `chromium` here is a stand-in that takes the
|
||||
# harness's arguments and drives the browser through Playwright instead.
|
||||
mkdir -p /opt/pw
|
||||
(cd /opt/pw && step 5m npm install --silent --no-audit --no-fund playwright-core@1.49.1 >/dev/null) || return 1
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers DEBIAN_FRONTEND=noninteractive \
|
||||
step 10m /opt/pw/node_modules/.bin/playwright-core install --with-deps chromium >/dev/null || return 1
|
||||
chmod -R a+rX /opt/pw /opt/pw-browsers
|
||||
cat >/opt/pw/shot.js <<'EOF'
|
||||
// node shot.js <url> <out.png> <width> <height>
|
||||
const { chromium } = require('/opt/pw/node_modules/playwright-core');
|
||||
(async () => {
|
||||
const [url, out, w, h] = process.argv.slice(2);
|
||||
const browser = await chromium.launch({ chromiumSandbox: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: Number(w), height: Number(h) }, ignoreHTTPSErrors: true });
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 30000 }).catch(() => {});
|
||||
await page.waitForTimeout(5000);
|
||||
await page.screenshot({ path: out });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(e => { console.error(e.message); process.exit(1); });
|
||||
EOF
|
||||
cat >/usr/local/bin/chromium <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stand-in for `chromium --headless ... --screenshot=<file> --window-size=W,H <url>`
|
||||
# on the ARM runner (see arm-runner-setup.sh). Other flags are ignored.
|
||||
url="" out="" w=1280 h=800
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--version) echo "Chromium (Playwright $(node -p "require('/opt/pw/node_modules/playwright-core/package.json').version"))"; exit 0 ;;
|
||||
--screenshot=*) out="${a#*=}" ;;
|
||||
--window-size=*) s="${a#*=}"; w="${s%,*}"; h="${s#*,}" ;;
|
||||
-*) ;;
|
||||
*) url="$a" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$url" && -n "$out" ]] || { echo "usage: chromium --screenshot=<file> <url>" >&2; exit 2; }
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers exec node /opt/pw/shot.js "$url" "$out" "$w" "$h"
|
||||
EOF
|
||||
chmod 755 /usr/local/bin/chromium
|
||||
# Ubuntu 24.04 forbids unprivileged user namespaces through AppArmor, and
|
||||
# Chromium's sandbox needs them. The sandbox stays on; the restriction goes,
|
||||
# on a VM that exists for this one job.
|
||||
sysctl -qw kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true
|
||||
|
||||
local uid fw net ns
|
||||
uid=$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
$fw -w -N CI_SHOT
|
||||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d "$NET_ADDR" -j REJECT
|
||||
iptables -w -A CI_SHOT -d "$NET_CIDR" -j ACCEPT
|
||||
for ns in 127.0.0.53 $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /run/systemd/resolve/resolv.conf 2>/dev/null); do
|
||||
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
|
||||
# The same call the harness makes, on a page that needs no network.
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
chown ci-shot "$dir"
|
||||
# HOME: runuser keeps root's, which ci-shot cannot write (the harness does the same).
|
||||
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
|
||||
--no-first-run --window-size=320,200 --user-data-dir="$dir/profile" \
|
||||
--screenshot="$dir/shot.png" 'data:text/html,<h1>ci</h1>' &>"$dir/chromium.log" || true
|
||||
if [[ ! -s "$dir/shot.png" ]]; then
|
||||
echo "Chromium could not take a test screenshot:"
|
||||
head -n 15 "$dir/chromium.log"
|
||||
echo "userns: $(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>&1)"
|
||||
rm -rf -- "$dir"
|
||||
return 1
|
||||
fi
|
||||
rm -rf -- "$dir"
|
||||
echo "$(chromium --version) ready for screenshots"
|
||||
}
|
||||
shots || echo "::warning::no screenshots on this ARM run (the build is still tested)"
|
||||
echo "Runner ready after $(($(date +%s) - start))s"
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs ci-vm-controller.sh on the hypervisor (prox1): the script, the
|
||||
# token reader, a systemd unit per CI VM, and each VM's config. Run as root
|
||||
# on prox1 from a directory that also holds ci-vm-controller.sh:
|
||||
#
|
||||
# scp ci-controller-setup.sh ci-vm-controller.sh root@prox1:/root/ci-controller/
|
||||
# ssh root@prox1 'cd /root/ci-controller && bash ci-controller-setup.sh'
|
||||
#
|
||||
# It does not start anything: a VM goes into service with
|
||||
# systemctl enable --now ci-vm-controller@<vmid>
|
||||
# once it has a 'clean' snapshot without any runner registered (see
|
||||
# docs/guides/pr-script-testing.md, "Single-use hosts").
|
||||
#
|
||||
# The token (/etc/ci-controller/token) is put there by hand: a fine-grained
|
||||
# token for the two repositories with Administration: read and write only.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
[[ -f ci-vm-controller.sh ]] || { echo "ci-vm-controller.sh missing next to this script" >&2; exit 1; }
|
||||
command -v jq >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq >/dev/null
|
||||
|
||||
install -m 755 ci-vm-controller.sh /usr/local/sbin/ci-vm-controller.sh
|
||||
install -d -m 700 /etc/ci-controller
|
||||
|
||||
cat >/usr/local/sbin/ci-gh-token <<'EOF'
|
||||
#!/bin/sh
|
||||
# Prints the token ci-vm-controller.sh uses (Administration: write on the CI
|
||||
# repositories). Swap this for a GitHub App token later without touching
|
||||
# the controller.
|
||||
exec cat /etc/ci-controller/token
|
||||
EOF
|
||||
chmod 755 /usr/local/sbin/ci-gh-token
|
||||
|
||||
cat >/etc/systemd/system/ci-vm-controller@.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Single-use CI runner host, VM %i
|
||||
After=network-online.target pve-cluster.service pvedaemon.service
|
||||
Wants=network-online.target
|
||||
# A missing token is a setup error, not something to retry every 30 s.
|
||||
ConditionPathExists=/etc/ci-controller/token
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/sbin/ci-vm-controller.sh %i
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
# Stopping the controller leaves the VM as it is; the next start resets it,
|
||||
# which also ends a job that may still be running there.
|
||||
KillMode=mixed
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# One repository per host: see ci-vm-controller.sh.
|
||||
conf() { # conf <vmid> <ip> <name> <label> <repo>
|
||||
cat >"/etc/ci-controller/$1.conf" <<EOF
|
||||
VM_IP=$2
|
||||
HOST_NAME=$3
|
||||
LABEL=$4
|
||||
REPOS=$5
|
||||
EOF
|
||||
}
|
||||
conf 200 10.201.146.232 pve-ci proxmox-test community-scripts/DevScripts
|
||||
conf 201 10.201.146.231 pve-ci-2 proxmox-test community-scripts/ProxmoxVE
|
||||
conf 204 10.201.146.235 pve-ci-3 proxmox-test community-scripts/ProxmoxVE
|
||||
conf 202 10.201.146.233 incus-ci incus-test community-scripts/DevScripts
|
||||
conf 203 10.201.146.234 incus-ci-2 incus-test community-scripts/ProxmoxVE
|
||||
|
||||
systemctl daemon-reload
|
||||
echo "Installed. Token: $([[ -s /etc/ci-controller/token ]] && echo present || echo 'MISSING (/etc/ci-controller/token)')"
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
# Keeps one CI VM (pve-ci, incus-ci, ...) as a single-use GitHub runner host.
|
||||
# Runs on the hypervisor (prox1), which PR code never touches.
|
||||
#
|
||||
# Every cycle:
|
||||
# 1. roll the VM back to its clean snapshot and boot it;
|
||||
# 2. register a just-in-time runner on it (generate-jitconfig) for the
|
||||
# host's repository. Such a runner takes exactly one job and is deleted
|
||||
# by GitHub afterwards, so its credentials are worthless once that job
|
||||
# is over;
|
||||
# 3. (with several repositories in REPOS: as soon as one runner is busy,
|
||||
# delete the others that are still idle, so no second job starts on a
|
||||
# host that is about to be reset);
|
||||
# 4. once every runner of this cycle is gone, start over at 1.
|
||||
#
|
||||
# PR code runs as root on the VM, so nothing the VM says is trusted: when a
|
||||
# job is over is read from GitHub's API, never from the VM.
|
||||
#
|
||||
# Config: /etc/ci-controller/<vmid>.conf (or the environment), see below.
|
||||
# Usage: ci-vm-controller.sh <vmid> run forever (the systemd unit)
|
||||
# ONE_CYCLE=1 ci-vm-controller.sh <vmid> one job, then stop (testing)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VMID=${1:?usage: ci-vm-controller.sh <vmid>}
|
||||
CONF=${CONF:-/etc/ci-controller/$VMID.conf}
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$CONF" ]] && . "$CONF"
|
||||
|
||||
: "${VM_IP:?VM_IP}" # address of the CI VM
|
||||
: "${HOST_NAME:?HOST_NAME}" # runner name prefix, e.g. pve-ci
|
||||
: "${LABEL:?LABEL}" # proxmox-test or incus-test
|
||||
# One repository per host. Several would each have a runner waiting on the
|
||||
# VM, and a job of one could read the idle runner's JIT config of the other
|
||||
# and use it elsewhere before it is withdrawn: one repository's PR code
|
||||
# faking the other's results. (Several are fine within one trust domain.)
|
||||
: "${REPOS:?REPOS, e.g. community-scripts/DevScripts}"
|
||||
EXTRA_LABELS=${EXTRA_LABELS:-} # space-separated, e.g. for a test
|
||||
SNAPSHOT=${SNAPSHOT:-clean}
|
||||
TOKEN_CMD=${TOKEN_CMD:-/usr/local/sbin/ci-gh-token} # prints a token with repo Administration: write
|
||||
PVE=${PVE:-} # prefix for qm, e.g. "ssh root@prox1" when not run on it
|
||||
GUEST_SSH=${GUEST_SSH:-"ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new root@$VM_IP"}
|
||||
POLL=${POLL:-20}
|
||||
MAX_BUSY_MIN=${MAX_BUSY_MIN:-180} # the test job's own limit is 90
|
||||
MAX_IDLE_MIN=${MAX_IDLE_MIN:-720} # recycle an idle host twice a day
|
||||
ONE_CYCLE=${ONE_CYCLE:-}
|
||||
|
||||
log() { echo "[$(date -u +%FT%TZ)] vm $VMID: $*"; }
|
||||
qm() { if [[ -n "$PVE" ]]; then $PVE qm "$@"; else command qm "$@"; fi; }
|
||||
|
||||
# Not in a subshell, so the status and the cached token survive the call:
|
||||
# api METHOD PATH [JSON] -> body in $API_BODY, HTTP status in $API_STATUS.
|
||||
API_FILE=$(mktemp)
|
||||
trap 'rm -f "$API_FILE"' EXIT
|
||||
TOKEN="" TOKEN_AT=0
|
||||
api() {
|
||||
local method=$1 path=$2 data=${3:-}
|
||||
# Cached for 30 minutes (an App installation token lives 60).
|
||||
if [[ -z "$TOKEN" || $(($(date +%s) - TOKEN_AT)) -gt 1800 ]]; then
|
||||
TOKEN=$($TOKEN_CMD) || { log "no GitHub token"; API_STATUS=000 API_BODY=""; return 1; }
|
||||
TOKEN_AT=$(date +%s)
|
||||
fi
|
||||
API_STATUS=$(curl -sS -o "$API_FILE" -w '%{http_code}' -X "$method" \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' ${data:+-d "$data"} "https://api.github.com$path") || API_STATUS=000
|
||||
API_BODY=$(<"$API_FILE")
|
||||
[[ "$API_STATUS" != 000 ]]
|
||||
}
|
||||
|
||||
reset_vm() {
|
||||
log "rolling back to '$SNAPSHOT'"
|
||||
[[ "$(qm status "$VMID")" == *running* ]] && qm stop "$VMID" --timeout 60 >/dev/null
|
||||
qm rollback "$VMID" "$SNAPSHOT" >/dev/null
|
||||
qm start "$VMID" >/dev/null
|
||||
local i
|
||||
for i in $(seq 1 60); do
|
||||
$GUEST_SSH true 2>/dev/null && { log "booted"; return 0; }
|
||||
sleep 5
|
||||
done
|
||||
log "no SSH after 5 minutes"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parallel arrays: one entry per runner registered in this cycle.
|
||||
R_REPO=() R_ID=() R_GONE=()
|
||||
|
||||
register() { # register <owner/repo>
|
||||
local repo=$1 name dir labels body cfg id
|
||||
name="$HOST_NAME-${repo#*/}-$(date +%s)"
|
||||
dir=/opt/actions-runner/${repo#*/}
|
||||
labels=$(printf '%s\n' "$LABEL" $EXTRA_LABELS | jq -R . | jq -sc .)
|
||||
api POST "/repos/$repo/actions/runners/generate-jitconfig" \
|
||||
"$(jq -nc --arg n "$name" --argjson l "$labels" '{name: $n, runner_group_id: 1, labels: $l, work_folder: "_work"}')" || return 1
|
||||
body=$API_BODY
|
||||
[[ "$API_STATUS" == 201 ]] || { log "$repo: generate-jitconfig failed ($API_STATUS): $(jq -r .message <<<"$body" 2>/dev/null)"; return 1; }
|
||||
id=$(jq -r .runner.id <<<"$body")
|
||||
cfg=$(jq -r .encoded_jit_config <<<"$body")
|
||||
# The config goes in through stdin, not the command line.
|
||||
if ! $GUEST_SSH "umask 077; cat >/run/ci-jit-${repo#*/} && systemd-run --quiet --collect --unit=ci-jit-${repo#*/} \
|
||||
-p WorkingDirectory=$dir -E RUNNER_ALLOW_RUNASROOT=1 \
|
||||
/bin/sh -c 'cfg=\$(cat /run/ci-jit-${repo#*/}); rm -f /run/ci-jit-${repo#*/}; exec ./run.sh --jitconfig \"\$cfg\"'" <<<"$cfg"; then
|
||||
log "$repo: could not start the runner on the VM"
|
||||
api DELETE "/repos/$repo/actions/runners/$id" >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
R_REPO+=("$repo") R_ID+=("$id") R_GONE+=(0)
|
||||
log "$repo: runner $name (id $id) started"
|
||||
}
|
||||
|
||||
drop() { # drop <index>: delete an idle runner; GitHub refuses a busy one
|
||||
local i=$1
|
||||
api DELETE "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" >/dev/null || return 1
|
||||
if [[ "$API_STATUS" == 204 || "$API_STATUS" == 404 ]]; then R_GONE[$i]=1; return 0; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
watch_cycle() {
|
||||
local start busy_since="" i body status busy any_busy left offline_since=""
|
||||
start=$(date +%s)
|
||||
while :; do
|
||||
sleep "$POLL"
|
||||
any_busy=0 left=0 offline=0
|
||||
for i in "${!R_ID[@]}"; do
|
||||
[[ "${R_GONE[$i]}" == 1 ]] && continue
|
||||
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || { left=1; continue; }
|
||||
body=$API_BODY
|
||||
if [[ "$API_STATUS" == 404 ]]; then
|
||||
R_GONE[$i]=1
|
||||
log "${R_REPO[$i]}: runner ${R_ID[$i]} is gone (job done or removed)"
|
||||
continue
|
||||
fi
|
||||
left=1
|
||||
busy=$(jq -r .busy <<<"$body"); status=$(jq -r .status <<<"$body")
|
||||
[[ "$busy" == true ]] && any_busy=1
|
||||
[[ "$status" == offline ]] && offline=1
|
||||
done
|
||||
if [[ "$any_busy" == 1 && -z "$busy_since" ]]; then
|
||||
busy_since=$(date +%s)
|
||||
log "a job started; withdrawing the idle runners"
|
||||
fi
|
||||
# A host that has started a job takes no other one: it will be reset.
|
||||
if [[ -n "$busy_since" ]]; then
|
||||
for i in "${!R_ID[@]}"; do
|
||||
[[ "${R_GONE[$i]}" == 1 ]] && continue
|
||||
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || continue
|
||||
body=$API_BODY
|
||||
[[ "$API_STATUS" == 200 && "$(jq -r .busy <<<"$body")" == false ]] && drop "$i" &&
|
||||
log "${R_REPO[$i]}: idle runner ${R_ID[$i]} withdrawn"
|
||||
done
|
||||
fi
|
||||
[[ "$left" == 0 ]] && { log "cycle over"; return 0; }
|
||||
|
||||
local now; now=$(date +%s)
|
||||
if [[ -n "$busy_since" && $((now - busy_since)) -gt $((MAX_BUSY_MIN * 60)) ]]; then
|
||||
log "job still running after $MAX_BUSY_MIN minutes; resetting anyway"
|
||||
break
|
||||
fi
|
||||
if [[ -z "$busy_since" && $((now - start)) -gt $((MAX_IDLE_MIN * 60)) ]]; then
|
||||
log "idle for $MAX_IDLE_MIN minutes; recycling"
|
||||
break
|
||||
fi
|
||||
# A runner that never comes online (or dies while idle) holds the host
|
||||
# forever otherwise.
|
||||
if [[ -z "$busy_since" && "$offline" == 1 ]]; then
|
||||
[[ -z "$offline_since" ]] && offline_since=$now
|
||||
if [[ $((now - offline_since)) -gt 300 ]]; then log "a runner is offline for 5 minutes; resetting"; break; fi
|
||||
else
|
||||
offline_since=""
|
||||
fi
|
||||
done
|
||||
for i in "${!R_ID[@]}"; do [[ "${R_GONE[$i]}" == 1 ]] || drop "$i" || true; done
|
||||
}
|
||||
|
||||
# Registrations of this host that a stopped or crashed controller left behind:
|
||||
# their VM was reset, so they are offline and never take a job, but they
|
||||
# clutter the runner list. Busy or online ones are left alone.
|
||||
cleanup_stale() {
|
||||
local repo ids id
|
||||
for repo in $REPOS; do
|
||||
api GET "/repos/$repo/actions/runners?per_page=100" || continue
|
||||
[[ "$API_STATUS" == 200 ]] || continue
|
||||
ids=$(jq -r --arg p "$HOST_NAME-${repo#*/}-" \
|
||||
'.runners[] | select((.name | startswith($p)) and .status == "offline" and (.busy | not)) | .id' <<<"$API_BODY")
|
||||
for id in $ids; do
|
||||
api DELETE "/repos/$repo/actions/runners/$id" && log "$repo: stale runner $id removed"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
while :; do
|
||||
R_REPO=() R_ID=() R_GONE=()
|
||||
if reset_vm; then
|
||||
cleanup_stale
|
||||
ok=0
|
||||
for repo in $REPOS; do register "$repo" && ok=1 || true; done
|
||||
if [[ "$ok" == 1 ]]; then watch_cycle; else log "no runner could be registered"; sleep 60; fi
|
||||
else
|
||||
sleep 60
|
||||
fi
|
||||
[[ -n "$ONE_CYCLE" ]] && { log "ONE_CYCLE: stopping; the VM is left as the job left it"; break; }
|
||||
done
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time setup of an Incus test host behind pr-script-test.yml: the same
|
||||
# ct/ scripts as on Proxmox, built through community-scripts/core's Incus
|
||||
# backend. Safe to re-run. Run as root on a disposable Debian 13 machine (it
|
||||
# builds pull request code as root).
|
||||
#
|
||||
# incus-ci-host-setup.sh host only
|
||||
# incus-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
# incus-ci-host-setup.sh <owner/repo> jit host + runner software only, for
|
||||
# ci-vm-controller.sh (single-use host)
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
NET_ADDR=10.98.0.1
|
||||
NET_CIDR=10.98.0.0/24
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq incus jq curl python3 iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
|
||||
|
||||
# Containers get a NATed bridge of their own (Incus runs DHCP and DNS on it)
|
||||
# and a plain directory pool on the root disk.
|
||||
if ! incus storage show default &>/dev/null; then
|
||||
incus admin init --preseed <<EOF
|
||||
networks:
|
||||
- name: incusbr0
|
||||
type: bridge
|
||||
config:
|
||||
ipv4.address: ${NET_ADDR}/24
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: none
|
||||
storage_pools:
|
||||
- name: default
|
||||
driver: dir
|
||||
profiles:
|
||||
- name: default
|
||||
devices:
|
||||
root:
|
||||
path: /
|
||||
pool: default
|
||||
type: disk
|
||||
eth0:
|
||||
name: eth0
|
||||
network: incusbr0
|
||||
type: nic
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Screenshots of the app under test are taken as this user, never as root.
|
||||
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
|
||||
# Same rules as on the Proxmox hosts: the screenshot browser may reach the
|
||||
# test containers, DNS and the public internet, nothing private, and not this
|
||||
# host's own address on the container network (the PR tree server).
|
||||
cat >/usr/local/sbin/ci-shot-firewall <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uid=\$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
\$fw -w -N CI_SHOT 2>/dev/null || \$fw -w -F CI_SHOT
|
||||
\$fw -w -C OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT 2>/dev/null ||
|
||||
\$fw -w -I OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d ${NET_ADDR} -j REJECT
|
||||
iptables -w -A CI_SHOT -d ${NET_CIDR} -j ACCEPT
|
||||
# systemd-resolved's stub; it forwards to the lab resolver itself.
|
||||
iptables -w -A CI_SHOT -d 127.0.0.53 -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d 127.0.0.53 -p tcp --dport 53 -j ACCEPT
|
||||
for ns in \$(awk '/^nameserver/ && \$2 !~ /:/ {print \$2}' /etc/resolv.conf /run/systemd/resolve/resolv.conf 2>/dev/null); do
|
||||
iptables -w -A CI_SHOT -d "\$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "\$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "\$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "\$net" -j REJECT
|
||||
done
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-shot-firewall
|
||||
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Keep the screenshot browser away from this host and the lab network
|
||||
After=network-online.target incus.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/sbin/ci-shot-firewall
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
[[ -s /etc/timezone ]] || {
|
||||
timedatectl set-timezone Europe/Vienna
|
||||
echo Europe/Vienna >/etc/timezone
|
||||
}
|
||||
|
||||
# The rewritten PR trees, served to the engine and to the containers. Waits
|
||||
# for incusbr0, which only exists once Incus is up.
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
cat >/etc/systemd/system/ci-serve.service <<EOF
|
||||
[Unit]
|
||||
Description=Serve pull request trees to script builds
|
||||
After=incus.service network-online.target
|
||||
Wants=incus.service
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/bin/sh -c 'for i in \$(seq 1 60); do ip -4 addr show dev incusbr0 | grep -q ${NET_ADDR} && exit 0; incus network list >/dev/null 2>&1; sleep 1; done; exit 1'
|
||||
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind ${NET_ADDR} --directory ${SERVE_ROOT}
|
||||
DynamicUser=yes
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Leftovers of a job that was killed before its cleanup trap ran.
|
||||
cat >/usr/local/sbin/ci-sweep <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Delete CI containers (ci-9000 .. ci-9099) that no running job holds, and stale trees.
|
||||
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
|
||||
# restart, timeout) never removes it, so a lock older than any job can run
|
||||
# (90 minutes) holds nothing.
|
||||
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
|
||||
for name in $(incus list -c n -f csv 2>/dev/null | grep -E '^ci-9[0-9]{3}$'); do
|
||||
held "$name" && continue
|
||||
incus delete "$name" --force &>/dev/null && echo "swept $name"
|
||||
done
|
||||
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
|
||||
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
|
||||
find /usr/local/community-scripts/logs /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
|
||||
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
|
||||
true
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-sweep
|
||||
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ci-sweep
|
||||
EOF
|
||||
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers every 3 hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=0/3:30
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
|
||||
systemctl restart ci-serve.service ci-shot-firewall.service
|
||||
|
||||
# --- Runner (one per repository) ----------------------------------------------
|
||||
if [[ $# -eq 2 ]]; then
|
||||
repo="$1"
|
||||
token="$2"
|
||||
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
|
||||
dir="/opt/actions-runner/${repo#*/}"
|
||||
mkdir -p "$dir"
|
||||
cd "$dir"
|
||||
if [[ ! -x ./config.sh ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
|
||||
# a fresh runner for every job and resets the VM afterwards, so only the
|
||||
# runner software goes on it, and the 'clean' snapshot is taken after this.
|
||||
if [[ "$token" == jit ]]; then
|
||||
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
|
||||
exit 0
|
||||
fi
|
||||
if [[ -f .runner ]]; then
|
||||
./svc.sh stop >/dev/null 2>&1 || true
|
||||
./svc.sh uninstall >/dev/null 2>&1 || true
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Only `incus-test`: see pve-ci-host-setup.sh for why there are no default labels.
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
|
||||
--url "https://github.com/${repo}" --token "$token" \
|
||||
--name "$(hostname -s)-${repo#*/}" --labels incus-test --work _work
|
||||
./svc.sh install root >/dev/null
|
||||
./svc.sh start >/dev/null
|
||||
echo "runner for ${repo} registered"
|
||||
fi
|
||||
|
||||
echo "incus test host ready: ${SERVE_ROOT} on ${NET_ADDR}:${SERVE_PORT}, sweeper every 3h"
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time setup of the Proxmox test host behind pr-script-test.yml.
|
||||
# Safe to re-run. Run as root on the test host (a disposable PVE, never a
|
||||
# production node: it builds pull request code as root).
|
||||
#
|
||||
# pve-ci-host-setup.sh host only
|
||||
# pve-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
# pve-ci-host-setup.sh <owner/repo> jit host + runner software only, for
|
||||
# ci-vm-controller.sh (single-use host)
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
UPLINK="${UPLINK:-vmbr0}"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq jq curl python3 dnsmasq iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
|
||||
|
||||
# Screenshots of the app under test are taken as this user, never as root.
|
||||
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
|
||||
# The page Chromium renders is served by code under test, and a redirect or an
|
||||
# <img> could point it at this host (the PVE UI on :8006) or the lab network;
|
||||
# the screenshot would then publish that on the PR. So ci-shot may reach the
|
||||
# test containers, DNS and the public internet (apps load fonts and scripts
|
||||
# from CDNs), and nothing private.
|
||||
cat >/usr/local/sbin/ci-shot-firewall <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uid=$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
$fw -w -N CI_SHOT 2>/dev/null || $fw -w -F CI_SHOT
|
||||
$fw -w -C OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT 2>/dev/null ||
|
||||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d 10.99.0.1 -j REJECT
|
||||
iptables -w -A CI_SHOT -d 10.99.0.0/24 -j ACCEPT
|
||||
for ns in $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /etc/resolv.conf); do
|
||||
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-shot-firewall
|
||||
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Keep the screenshot browser away from this host and the lab network
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/sbin/ci-shot-firewall
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Containers get their own NATed bridge instead of the lab network: no DHCP
|
||||
# lease surprises (a lab lease once handed a container the host's own address),
|
||||
# and an app listening on 8080 can never be confused with this host.
|
||||
cat >/etc/network/interfaces.d/ci-nat <<EOF
|
||||
auto vmbr1
|
||||
iface vmbr1 inet static
|
||||
address 10.99.0.1/24
|
||||
bridge-ports none
|
||||
bridge-stp off
|
||||
bridge-fd 0
|
||||
post-up sysctl -qw net.ipv4.ip_forward=1
|
||||
post-up iptables -t nat -C POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE
|
||||
post-down iptables -t nat -D POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE || true
|
||||
EOF
|
||||
ifreload -a
|
||||
cat >/etc/dnsmasq.d/ci.conf <<'EOF'
|
||||
interface=vmbr1
|
||||
bind-interfaces
|
||||
except-interface=lo
|
||||
dhcp-range=10.99.0.100,10.99.0.250,255.255.255.0,2h
|
||||
dhcp-option=option:router,10.99.0.1
|
||||
dhcp-option=option:dns-server,10.99.0.1
|
||||
no-resolv
|
||||
EOF
|
||||
# Upstream = whatever this host resolves with. Debian's dnsmasq would otherwise
|
||||
# ask resolvconf, which has nothing to offer on a PVE host.
|
||||
awk '/^nameserver/ {print "server=" $2}' /etc/resolv.conf >>/etc/dnsmasq.d/ci.conf
|
||||
systemctl enable dnsmasq >/dev/null
|
||||
systemctl restart dnsmasq
|
||||
|
||||
# A PVE ISO install writes /etc/timezone; a cloud-image install does not, and
|
||||
# the engine's pct_timezone aborts without it.
|
||||
[[ -s /etc/timezone ]] || {
|
||||
timedatectl set-timezone Europe/Vienna
|
||||
echo Europe/Vienna >/etc/timezone
|
||||
}
|
||||
|
||||
# Containers and VM disks live on the root filesystem; there is no LVM in a
|
||||
# cloud-image PVE. Snippets hold the Cloud-Init configs some vm/ scripts write.
|
||||
pvesm set local --content iso,vztmpl,backup,rootdir,images,snippets
|
||||
|
||||
# Templates are fetched once here rather than on every first build.
|
||||
pveam update >/dev/null
|
||||
for t in $(pveam available --section system | awk '/_amd64\./ && /debian-13-standard|alpine-3\.2[0-9]-default|ubuntu-24\.04-standard/ {print $2}'); do
|
||||
pveam list local | grep -q "$t" || pveam download local "$t" >/dev/null
|
||||
done
|
||||
|
||||
# The rewritten PR trees, served to the engine and to the containers.
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
cat >/etc/systemd/system/ci-serve.service <<EOF
|
||||
[Unit]
|
||||
Description=Serve pull request trees to script builds
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind 10.99.0.1 --directory ${SERVE_ROOT}
|
||||
DynamicUser=yes
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Leftovers of a job that was killed before its cleanup trap ran.
|
||||
cat >/usr/local/sbin/ci-sweep <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Destroy CI containers (9000-9099) and VMs (9100-9199) that no running job
|
||||
# holds, and stale trees.
|
||||
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
|
||||
# restart, timeout) never removes it, so a lock older than any job can run
|
||||
# (90 minutes) holds nothing.
|
||||
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
|
||||
for id in $(pct list | awk 'NR>1 && $1>=9000 && $1<=9099 {print $1}'); do
|
||||
held "$id" && continue
|
||||
pct stop "$id" --skiplock 1 &>/dev/null || true
|
||||
pct destroy "$id" --purge 1 --force 1 &>/dev/null && echo "swept CT $id"
|
||||
done
|
||||
for id in $(qm list | awk 'NR>1 && $1>=9100 && $1<=9199 {print $1}'); do
|
||||
held "$id" && continue
|
||||
qm stop "$id" --skiplock 1 --timeout 30 &>/dev/null || true
|
||||
qm destroy "$id" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null && echo "swept VM $id"
|
||||
done
|
||||
# VM images and ISOs are cached between builds; drop the ones no build used
|
||||
# for a week. Container templates (*.tar.*) stay.
|
||||
find /var/lib/vz/template/cache /var/lib/vz/template/iso -maxdepth 1 -type f ! -name '*.tar.*' -atime +7 -delete 2>/dev/null
|
||||
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
|
||||
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
|
||||
# Engine logs that a killed job never got to remove.
|
||||
find /tmp -maxdepth 1 -type f \( -name 'create-lxc-*.log' -o -name 'pct_create_*.log' \
|
||||
-o -regex '/tmp/.*-9[0-9][0-9][0-9]-[0-9a-f]+\.log' \) -mmin +180 -delete
|
||||
find /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
|
||||
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
|
||||
true
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-sweep
|
||||
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ci-sweep
|
||||
EOF
|
||||
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers every 3 hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=0/3:30
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
|
||||
systemctl restart ci-serve.service ci-shot-firewall.service
|
||||
|
||||
# --- Runner (one per repository) ----------------------------------------------
|
||||
if [[ $# -eq 2 ]]; then
|
||||
repo="$1"
|
||||
token="$2"
|
||||
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
|
||||
dir="/opt/actions-runner/${repo#*/}"
|
||||
mkdir -p "$dir"
|
||||
cd "$dir"
|
||||
if [[ ! -x ./config.sh ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
|
||||
# a fresh runner for every job and resets the VM afterwards, so only the
|
||||
# runner software goes on it, and the 'clean' snapshot is taken after this.
|
||||
if [[ "$token" == jit ]]; then
|
||||
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
|
||||
exit 0
|
||||
fi
|
||||
# A registered runner is reconfigured from scratch: stop and remove the old
|
||||
# registration (the token works for removal too).
|
||||
if [[ -f .runner ]]; then
|
||||
./svc.sh stop >/dev/null 2>&1 || true
|
||||
./svc.sh uninstall >/dev/null 2>&1 || true
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Root, because pct needs it; the host is disposable by design.
|
||||
# --no-default-labels: without it the runner also answers to `self-hosted`,
|
||||
# and any other workflow with `runs-on: self-hosted` (and its secrets) could
|
||||
# land on this host.
|
||||
# Named after this host: with --replace, a fixed name would make a second
|
||||
# test host take over the first one's registration instead of adding to it.
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
|
||||
--url "https://github.com/${repo}" --token "$token" \
|
||||
--name "$(hostname -s)-${repo#*/}" --labels proxmox-test --work _work
|
||||
./svc.sh install root >/dev/null
|
||||
./svc.sh start >/dev/null
|
||||
echo "runner for ${repo} registered"
|
||||
fi
|
||||
|
||||
echo "test host ready: ${SERVE_ROOT} on :${SERVE_PORT}, sweeper every 3h"
|
||||
+824
@@ -0,0 +1,824 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build one ct/ or vm/ script from a pull request on this test host, check
|
||||
# that the container or VM really works, destroy it, and leave a result.json
|
||||
# behind. The vm/ part lives in pve-run-vm.sh, sourced from here.
|
||||
#
|
||||
# PLATFORM says what this host is: pve (Proxmox VE, pct) or incus. The ct/
|
||||
# scripts are the same for both; community-scripts/core picks its Incus
|
||||
# backend by itself when it finds incus instead of pveversion. Only the way
|
||||
# this harness reaches into the container differs (guest_* below).
|
||||
#
|
||||
# Runs on the self-hosted runner, which *is* the disposable test host. The
|
||||
# harness comes from the default branch; only the script under test comes from
|
||||
# the pull request, pinned to the commit a maintainer approved.
|
||||
#
|
||||
# The engine hardcodes raw.githubusercontent.com/<upstream>/main for every fetch
|
||||
# (ct -> build.func -> install script -> install.func -> tools.func). Pointing it
|
||||
# at a fork branch therefore means rewriting those URLs: the PR tree is unpacked
|
||||
# under SERVE_ROOT, every upstream main URL in it is replaced by this host's own
|
||||
# HTTP server, and the containers fetch from there. Engine changes in the PR are
|
||||
# tested along with the script, which is what a reviewer wants anyway.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
: "${PR_REPO:?}" "${PR_SHA:?}" "${SCRIPT:?}" "${UPSTREAM:?}" "${CTID_BASE:?}" "${OUT_DIR:?}"
|
||||
OS="${OS:-}"
|
||||
PLATFORM="${PLATFORM:-pve}"
|
||||
CTID_COUNT="${CTID_COUNT:-50}"
|
||||
SERVE_ROOT="${SERVE_ROOT:-/srv/ci}"
|
||||
# The containers' own network: vmbr1 + dnsmasq on Proxmox, incusbr0 on Incus.
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
SERVE_URL="${SERVE_URL:-http://10.98.0.1:18080}"
|
||||
BRIDGE="${BRIDGE:-incusbr0}"
|
||||
else
|
||||
SERVE_URL="${SERVE_URL:-http://10.99.0.1:18080}"
|
||||
BRIDGE="${BRIDGE:-vmbr1}"
|
||||
fi
|
||||
BUILD_TIMEOUT="${BUILD_TIMEOUT:-45m}"
|
||||
URL_WAIT="${URL_WAIT:-120}"
|
||||
|
||||
# Every value below reaches a path, a URL or a command line.
|
||||
[[ "$PR_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad PR_REPO" >&2; exit 2; }
|
||||
[[ "$UPSTREAM" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad UPSTREAM" >&2; exit 2; }
|
||||
[[ "$PR_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "bad PR_SHA" >&2; exit 2; }
|
||||
[[ "$SCRIPT" =~ ^(ct|vm)/[A-Za-z0-9._-]+\.sh$ ]] || { echo "bad SCRIPT" >&2; exit 2; }
|
||||
[[ -z "$OS" || "$OS" =~ ^(debian|ubuntu|alpine)$ ]] || { echo "bad OS" >&2; exit 2; }
|
||||
[[ "$PLATFORM" =~ ^(pve|incus)$ ]] || { echo "bad PLATFORM" >&2; exit 2; }
|
||||
# yes: build with var_arm64=yes. On arm64 the engine refuses to guess for a
|
||||
# script that leaves var_arm64 unset; the ARM run builds those anyway, to see.
|
||||
VAR_ARM64="${VAR_ARM64:-}"
|
||||
[[ -z "$VAR_ARM64" || "$VAR_ARM64" == "yes" ]] || { echo "bad VAR_ARM64" >&2; exit 2; }
|
||||
# upgrade: install the script as it is on the base branch (BASE_SHA of
|
||||
# UPSTREAM), then update with the PR. That is what a user does who installed
|
||||
# before the PR is merged and runs `update` after it. Empty: install and
|
||||
# update both from the PR.
|
||||
TEST_MODE="${TEST_MODE:-}"
|
||||
[[ -z "$TEST_MODE" || "$TEST_MODE" == "upgrade" ]] || { echo "bad TEST_MODE" >&2; exit 2; }
|
||||
if [[ -n "$TEST_MODE" ]]; then
|
||||
[[ "${BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || { echo "bad BASE_SHA" >&2; exit 2; }
|
||||
[[ "$SCRIPT" == ct/* ]] || { echo "upgrade tests are for ct/ scripts" >&2; exit 2; }
|
||||
fi
|
||||
[[ "$PLATFORM" == "pve" || "$SCRIPT" == ct/* ]] || { echo "vm/ scripts run on pve only" >&2; exit 2; }
|
||||
[[ "$CTID_BASE" =~ ^[0-9]+$ ]] || { echo "bad CTID_BASE" >&2; exit 2; }
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
RAW_LOG="$OUT_DIR/build.raw.log"
|
||||
LOG="$OUT_DIR/build.log"
|
||||
RESULT="$OUT_DIR/result.json"
|
||||
INSTALL_LOG_OUT="$OUT_DIR/install.log"
|
||||
UPDATE_RAW="$OUT_DIR/update.raw.log"
|
||||
UPDATE_LOG="$OUT_DIR/update.log"
|
||||
UPDATE_TIMEOUT="${UPDATE_TIMEOUT:-30m}"
|
||||
UPDATE2_RAW="$OUT_DIR/update2.raw.log"
|
||||
UPDATE2_LOG="$OUT_DIR/update2.log"
|
||||
# How long the app has to keep running without a service restarting.
|
||||
STABLE_WAIT="${STABLE_WAIT:-60}"
|
||||
|
||||
kind="${SCRIPT%%/*}" # ct or vm
|
||||
app=$(basename "$SCRIPT" .sh)
|
||||
job="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${PLATFORM}-${kind}-${app}-${OS:-default}${TEST_MODE:+-$TEST_MODE}"
|
||||
tree="$SERVE_ROOT/$job"
|
||||
base="$SERVE_URL/$job"
|
||||
src_tree="$tree" # what gets installed; the base branch's tree in an upgrade test
|
||||
started=$(date +%s)
|
||||
|
||||
status="fail"
|
||||
step=""
|
||||
exit_code=""
|
||||
ctid="" # the guest's ID (pve: 9000, incus: ci-9000), container or VM
|
||||
|
||||
# The container, whatever the platform. ctid is set once a slot is reserved.
|
||||
guest_exec() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then incus exec "$ctid" -- "$@"; else pct exec "$ctid" -- "$@"; fi
|
||||
}
|
||||
guest_exists() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then incus info "$ctid" &>/dev/null; else pct status "$ctid" &>/dev/null; fi
|
||||
}
|
||||
guest_running() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
[[ "$(incus list "^${ctid}\$" -c s -f csv 2>/dev/null)" == "RUNNING" ]]
|
||||
else
|
||||
pct status "$ctid" 2>/dev/null | grep -q running
|
||||
fi
|
||||
}
|
||||
guest_destroy() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
incus delete "$ctid" --force &>/dev/null || true
|
||||
else
|
||||
pct stop "$ctid" --skiplock 1 &>/dev/null || true
|
||||
pct destroy "$ctid" --purge 1 --force 1 &>/dev/null || true
|
||||
fi
|
||||
}
|
||||
ct_ip=""
|
||||
url=""
|
||||
http_code=""
|
||||
failed_units=""
|
||||
# updated: a real update ran; no-update: update ran, nothing to update;
|
||||
# fail: the update broke; empty: not attempted (the install failed).
|
||||
update_status=""
|
||||
update_step=""
|
||||
# install: building and checking what was installed; update: from the first
|
||||
# update on. In an upgrade test a failure while installing is the base
|
||||
# branch's, not the PR's.
|
||||
phase="install"
|
||||
# Findings that do not fail the build, shown under the table on the PR.
|
||||
notes=()
|
||||
# Disk, RAM and OOM kills of the container, measured at the end.
|
||||
resources=""
|
||||
# Second `update` right after the first: no-update, updated-again, or empty.
|
||||
update2_status=""
|
||||
|
||||
# Spinner frames: the engine redraws "⠋ Starting VM" a few times a second
|
||||
# without a tty too. An alternation, not a bracket: these are multibyte and
|
||||
# the runner's locale may be C.
|
||||
SPINNER_RE='^\s*(⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏)'
|
||||
|
||||
strip_ansi() {
|
||||
sed -E 's/\x1B\[[0-9;?]*[A-Za-z]//g; s/\x1B[()][A-Z0-9]//g; s/\r/\n/g' | grep -vE '^\s*$' | grep -vE "$SPINNER_RE"
|
||||
}
|
||||
|
||||
# The install script's own output: everything $STD hides from the build log.
|
||||
# Both engines write it inside the container as /root/.install-<session>.log;
|
||||
# dev_mode=keep makes sure a failed container is still there to read it from.
|
||||
# If it is gone anyway, the copy the engine leaves on the host is the fallback.
|
||||
collect_install_log() {
|
||||
[[ -n "$ctid" ]] || return 0
|
||||
# Exactly one build ran in this container, so the newest install log is ours.
|
||||
if guest_exists &&
|
||||
guest_exec sh -c 'cat "$(ls -t /root/.install-*.log 2>/dev/null | head -n1)"' 2>/dev/null |
|
||||
strip_ansi >"$INSTALL_LOG_OUT" && [[ -s "$INSTALL_LOG_OUT" ]]; then
|
||||
return 0
|
||||
fi
|
||||
[[ "$PLATFORM" == "pve" ]] || { rm -f "$INSTALL_LOG_OUT"; return 0; }
|
||||
# The engine names its host-side files after the session: pct_create_<ctid>_<date>_<session>.log
|
||||
local sid f
|
||||
sid=$(find /tmp -maxdepth 1 -name "pct_create_${ctid}_*.log" -newer "$RAW_LOG.start" -printf '%f\n' 2>/dev/null |
|
||||
sed -E 's/.*_([0-9a-f]+)\.log$/\1/' | head -n1)
|
||||
[[ -n "$sid" ]] || { rm -f "$INSTALL_LOG_OUT"; return 0; }
|
||||
for f in /tmp/*-"${ctid}"-"${sid}".log /var/log/community-scripts/install-"${sid}"-combined-*.log; do
|
||||
[[ -s "$f" ]] && { strip_ansi <"$f" >"$INSTALL_LOG_OUT"; return 0; }
|
||||
done
|
||||
rm -f "$INSTALL_LOG_OUT"
|
||||
}
|
||||
|
||||
# "Requires AVX2" and friends: the script may be fine, the test host's CPU is
|
||||
# not (Xeon E5-2650, Sandy Bridge: AVX, no AVX2). Shown as untestable rather
|
||||
# than as a failure of the PR -- but the log is written by the code under test,
|
||||
# so its word is not enough. Only the engine's error lines count, only when the
|
||||
# build or install itself failed, and the feature they name must really be
|
||||
# missing from this host's /proc/cpuinfo. Claiming a feature the host has
|
||||
# changes nothing.
|
||||
CPU_RE='(requir|need)[a-z]*[^.]{0,60}(AVX(2|-?512)?|SSE4[._]?2|x86-64-v[234])\b|\b(AVX(2|-?512)?|x86-64-v[234])\b[^.]{0,60}(not (supported|available)|does not have|is missing|unsupported)'
|
||||
|
||||
classify_cpu_failure() {
|
||||
local line feature flags
|
||||
line=$(strip_ansi <"$RAW_LOG" | grep -E '✖|\[ERROR\]' | grep -m1 -iE "$CPU_RE" |
|
||||
sed -E 's/^\s*(✖️?|\[[^]]*\]|\s)*//' | cut -c1-200)
|
||||
[[ -n "$line" ]] || return 0
|
||||
# The CPU flag that would have to be missing for the message to be true.
|
||||
case "${line,,}" in
|
||||
*avx-512* | *avx512* | *x86-64-v4*) feature=avx512f ;;
|
||||
*avx2* | *x86-64-v3*) feature=avx2 ;;
|
||||
*sse4.2* | *sse4_2* | *x86-64-v2*) feature=sse4_2 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
flags=" $(grep -m1 '^flags' /proc/cpuinfo | cut -d: -f2) "
|
||||
[[ "$flags" == *" ${feature} "* ]] && return 0
|
||||
status="unsupported"
|
||||
step="needs a newer CPU than the test host has (no ${feature}): ${line}"
|
||||
}
|
||||
|
||||
# Disk used against var_disk, RAM in use at rest (anonymous memory, without
|
||||
# the page cache that fills any limit) against the limit, and how often the
|
||||
# kernel killed something for lack of memory, over the container's whole life,
|
||||
# the build included. A dir-backed Incus root has no size of its own: df shows
|
||||
# the host's disk. Then what the container holds is counted with du, and the
|
||||
# script's var_disk is the size to compare with.
|
||||
measure_resources() {
|
||||
local out disk_size disk_used anon limit oom want_disk want_ram line=()
|
||||
out=$(guest_exec sh -c 'df -Pm / | awk "NR==2 {print \$2, \$3}"
|
||||
awk "/^anon / {print int(\$2 / 1048576)}" /sys/fs/cgroup/memory.stat 2>/dev/null || echo -
|
||||
cat /sys/fs/cgroup/memory.max 2>/dev/null || echo -
|
||||
awk "/^oom_kill / {print \$2}" /sys/fs/cgroup/memory.events 2>/dev/null || echo -' 2>/dev/null) || return 0
|
||||
read -r disk_size disk_used <<<"$(sed -n 1p <<<"$out")"
|
||||
anon=$(sed -n 2p <<<"$out"); limit=$(sed -n 3p <<<"$out"); oom=$(sed -n 4p <<<"$out")
|
||||
want_disk=$(grep -m1 -oE 'var_disk="\$\{var_disk:-[0-9]+\}"' "$src_tree/$SCRIPT" | grep -oE '[0-9]+' | head -n1)
|
||||
want_ram=$(grep -m1 -oE 'var_ram="\$\{var_ram:-[0-9]+\}"' "$src_tree/$SCRIPT" | grep -oE '[0-9]+' | head -n1)
|
||||
[[ "$disk_size" =~ ^[0-9]+$ && "$disk_used" =~ ^[0-9]+$ ]] || return 0
|
||||
if [[ -n "$want_disk" ]] && ((disk_size > want_disk * 1024 * 3 / 2)); then
|
||||
disk_size=$((want_disk * 1024))
|
||||
disk_used=$(guest_exec sh -c 'du -sxm / 2>/dev/null | cut -f1' 2>/dev/null)
|
||||
[[ "$disk_used" =~ ^[0-9]+$ ]] || return 0
|
||||
fi
|
||||
line+=("disk $(awk -v u="$disk_used" 'BEGIN {printf "%.1f", u / 1024}') of $(awk -v s="$disk_size" 'BEGIN {printf "%.0f", s / 1024}') GB")
|
||||
if ((disk_used * 100 >= disk_size * 90)); then
|
||||
notes+=("disk ${disk_used} of ${disk_size} MB used after the install: var_disk may be too small")
|
||||
fi
|
||||
[[ "$limit" =~ ^[0-9]+$ ]] && limit=$((limit / 1048576)) || limit="${want_ram:-}"
|
||||
if [[ "$anon" =~ ^[0-9]+$ ]]; then
|
||||
line+=("RAM at rest ${anon}${limit:+ of ${limit}} MB")
|
||||
if [[ "$limit" =~ ^[0-9]+$ ]] && ((anon * 100 >= limit * 80)); then
|
||||
notes+=("${anon} of ${limit} MB RAM in use at rest: var_ram may be too small")
|
||||
fi
|
||||
fi
|
||||
if [[ "$oom" =~ ^[0-9]+$ ]]; then
|
||||
line+=("OOM kills ${oom}")
|
||||
((oom > 0)) && notes+=("the kernel killed ${oom} process(es) for lack of memory: var_ram is too small for the build or the app")
|
||||
fi
|
||||
resources=$(
|
||||
IFS='|'
|
||||
echo "${line[*]}"
|
||||
)
|
||||
resources=${resources//|/ · }
|
||||
}
|
||||
|
||||
finish() {
|
||||
[[ -f "$RAW_LOG" ]] && strip_ansi <"$RAW_LOG" >"$LOG"
|
||||
[[ -f "$RAW_LOG" ]] && collect_install_log
|
||||
[[ -f "$UPDATE_RAW" ]] && strip_ansi <"$UPDATE_RAW" >"$UPDATE_LOG"
|
||||
[[ -f "$UPDATE2_RAW" ]] && strip_ansi <"$UPDATE2_RAW" >"$UPDATE2_LOG"
|
||||
rm -f "$UPDATE_RAW" "$UPDATE2_RAW"
|
||||
[[ "$kind" == ct && -n "$ctid" && -z "$resources" ]] && guest_running && measure_resources
|
||||
jq -n \
|
||||
--arg script "$SCRIPT" --arg kind "$kind" --arg platform "$PLATFORM" --arg os "${OS:-default}" --arg status "$status" \
|
||||
--arg mode "$TEST_MODE" --arg resources "$resources" --arg update2_status "$update2_status" \
|
||||
--arg notes "$(printf '%s\n' "${notes[@]}")" \
|
||||
--arg step "$step" --arg exit_code "$exit_code" --arg sha "$PR_SHA" \
|
||||
--arg ctid "$ctid" --arg url "$url" --arg http_code "$http_code" \
|
||||
--arg failed_units "$failed_units" --arg ip "$ct_ip" --arg arch "$(uname -m)" \
|
||||
--arg update_status "$update_status" --arg update_step "$update_step" \
|
||||
--argjson duration "$(($(date +%s) - started))" \
|
||||
--arg tail "$([[ -f "$LOG" ]] && tail -n 60 "$LOG")" \
|
||||
'{script: $script, kind: $kind, platform: $platform, arch: $arch, os: $os, status: $status, step: $step, exit_code: $exit_code,
|
||||
sha: $sha, ctid: $ctid, ip: $ip, url: $url, http_code: $http_code,
|
||||
failed_units: $failed_units, update_status: $update_status, update_step: $update_step,
|
||||
mode: $mode, resources: $resources, update2_status: $update2_status,
|
||||
notes: ($notes | split("\n") | map(select(. != ""))),
|
||||
duration: $duration, log_tail: $tail}' >"$RESULT"
|
||||
echo "::group::result"
|
||||
jq 'del(.log_tail)' "$RESULT"
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
# --- Live output ------------------------------------------------------------------
|
||||
# The job log on GitHub streams while the job runs, so mirroring the build there
|
||||
# is all "live" takes. Two streams: the engine's own output, and the install
|
||||
# log inside the container, which holds everything $STD hides (apt, npm, ...).
|
||||
#
|
||||
# Both are written by the code under test. A line starting with `::` would be
|
||||
# read by the runner as a workflow command, so command processing is switched
|
||||
# off for as long as they run, with a token the build cannot know.
|
||||
stream_token=""
|
||||
log_pid=""
|
||||
install_pid=""
|
||||
|
||||
# Colours stay, cursor movement and spinner redraws go.
|
||||
live_filter() {
|
||||
sed -u -E 's/\r/\n/g; s/\x1B\[[0-9;?]*[ABCDEFGHJKSTfhilnsu]//g' | sed -u -E "/^\s*\$/d; /${SPINNER_RE}/d"
|
||||
}
|
||||
|
||||
start_streams() {
|
||||
stream_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
echo "::stop-commands::${stream_token}"
|
||||
tail -n +1 -F --pid="$build_pid" "$RAW_LOG" 2>/dev/null > >(live_filter) &
|
||||
log_pid=$!
|
||||
# A VM has no install log to follow: its script runs on this host only.
|
||||
[[ "$kind" == "ct" ]] || return 0
|
||||
# setsid: its own process group, so stop_streams can take down the
|
||||
# pct/incus exec + tail pair inside it in one go.
|
||||
setsid bash -c '
|
||||
gx() { if [ "$2" = incus ]; then incus exec "$1" -- "${@:3}"; else pct exec "$1" -- "${@:3}"; fi; }
|
||||
for _ in $(seq 1 900); do
|
||||
f=$(gx "$1" "$2" sh -c "ls -t /root/.install-*.log 2>/dev/null | head -n1" 2>/dev/null)
|
||||
if [ -n "$f" ]; then
|
||||
echo "──────── install log of CT $1 (live) ────────"
|
||||
if [ "$2" = incus ]; then exec incus exec "$1" -- tail -n +1 -F "$f"; fi
|
||||
exec pct exec "$1" -- tail -n +1 -F "$f"
|
||||
fi
|
||||
sleep 2
|
||||
done' _ "$ctid" "$PLATFORM" 2>/dev/null > >(live_filter | sed -u 's/^/ │ /') &
|
||||
install_pid=$!
|
||||
}
|
||||
|
||||
stop_streams() {
|
||||
[[ -n "$stream_token" ]] || return 0
|
||||
[[ -n "$install_pid" ]] && kill -TERM -- "-$install_pid" &>/dev/null
|
||||
[[ -n "$log_pid" ]] && kill -TERM "$log_pid" &>/dev/null
|
||||
wait "$log_pid" "$install_pid" &>/dev/null
|
||||
sleep 1 # let the filters flush
|
||||
install_pid=""
|
||||
log_pid=""
|
||||
echo "::${stream_token}::"
|
||||
stream_token=""
|
||||
}
|
||||
|
||||
build_pid=""
|
||||
update_pid=""
|
||||
cleanup() {
|
||||
stop_streams
|
||||
local pid
|
||||
for pid in "$build_pid" "$update_pid"; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
pkill -TERM -P "$pid" &>/dev/null || true
|
||||
kill -TERM "$pid" &>/dev/null || true
|
||||
sleep 2
|
||||
done
|
||||
if [[ -n "$ctid" && "$kind" == "vm" ]]; then
|
||||
vm_cleanup
|
||||
elif [[ -n "$ctid" ]]; then
|
||||
guest_exists && guest_destroy
|
||||
rm -f "/run/ci-jobs/$ctid"
|
||||
# What the engine leaves on the host for this container and its session.
|
||||
local f sid
|
||||
[[ "$PLATFORM" == "pve" ]] && for f in /tmp/pct_create_"${ctid}"_*.log; do
|
||||
[[ -f "$f" && "$f" -nt "$RAW_LOG.start" ]] || continue
|
||||
sid=$(sed -E 's/.*_([0-9a-f]+)\.log$/\1/' <<<"$f")
|
||||
rm -f -- "$f" /tmp/create-lxc-"${sid}".log /tmp/*-"${ctid}"-"${sid}".log
|
||||
done
|
||||
fi
|
||||
rm -f "$RAW_LOG.start"
|
||||
rm -rf -- "$tree" "$tree-base"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
# A cancelled job sends TERM; without this the EXIT trap would not run.
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
|
||||
# fail <reason> [fail|unsupported]: only the build/install branches pass the
|
||||
# second argument, after classify_cpu_failure has checked the host.
|
||||
#
|
||||
# The exit code says whether *this job* worked, not whether the app did. A
|
||||
# script that fails to build is a result: it is written down, reported on the
|
||||
# PR (check, comment, review) and the job exits 0 -- a red run per broken app
|
||||
# would only be notification noise. Only trouble with the test setup itself
|
||||
# (setup_error) fails the job, because that is ours to look at.
|
||||
fail() {
|
||||
step="$1"
|
||||
status="${2:-fail}"
|
||||
[[ "$status" == "unsupported" ]] || status="fail"
|
||||
if [[ "$TEST_MODE" == "upgrade" && "$phase" == "install" && "$status" == "fail" ]]; then
|
||||
status="error"
|
||||
step="the install before this PR (${UPSTREAM}@${BASE_SHA:0:7}) failed, so the upgrade was not tested: $1"
|
||||
fi
|
||||
finish
|
||||
echo "::warning title=${SCRIPT}::${step}"
|
||||
exit 0
|
||||
}
|
||||
|
||||
setup_error() {
|
||||
step="$1"
|
||||
status="error"
|
||||
finish
|
||||
echo "::error title=${SCRIPT}::test setup: ${step}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Reserve a container ID ---------------------------------------------------
|
||||
# A lock file per ID, not just "first free one", because the engine only creates
|
||||
# the container minutes later and two jobs would otherwise pick the same ID.
|
||||
# VMs take the range 100 above the containers' (DevScripts 9100-9149,
|
||||
# ProxmoxVE 9150-9199).
|
||||
mkdir -p /run/ci-jobs
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
source "$(dirname "$0")/pve-run-vm.sh"
|
||||
CTID_BASE=$((CTID_BASE + 100))
|
||||
vm_wait_turn
|
||||
fi
|
||||
exec 9>/run/ci-jobs/.lock
|
||||
flock 9
|
||||
# On Incus the ID is an instance name: var_ctid=ci-9000 names the container.
|
||||
for ((id = CTID_BASE; id < CTID_BASE + CTID_COUNT; id++)); do
|
||||
name=$id
|
||||
[[ "$PLATFORM" == "incus" ]] && name="ci-$id"
|
||||
[[ -e "/run/ci-jobs/$name" ]] && continue
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
incus info "$name" &>/dev/null && continue
|
||||
else
|
||||
pvesh get /cluster/nextid --vmid "$id" &>/dev/null || continue
|
||||
fi
|
||||
echo "$job" >"/run/ci-jobs/$name"
|
||||
ctid=$name
|
||||
break
|
||||
done
|
||||
flock -u 9
|
||||
exec 9>&-
|
||||
[[ -n "$ctid" ]] || setup_error "no free ${kind} ID in ${CTID_BASE}-$((CTID_BASE + CTID_COUNT - 1))"
|
||||
|
||||
# --- Fetch the approved commit and point it at ourselves ------------------------
|
||||
mkdir -p "$tree"
|
||||
curl -fsSL --retry 3 "https://codeload.github.com/${PR_REPO}/tar.gz/${PR_SHA}" |
|
||||
tar -xz -C "$tree" --strip-components=1 || setup_error "could not download ${PR_REPO}@${PR_SHA:0:7}"
|
||||
[[ -f "$tree/$SCRIPT" ]] || fail "${SCRIPT} does not exist at ${PR_SHA:0:7}"
|
||||
|
||||
upstream_re="https://raw\.githubusercontent\.com/${UPSTREAM//./\\.}/(refs/heads/)?main"
|
||||
grep -rlE "$upstream_re" "$tree" --include='*.sh' --include='*.func' |
|
||||
xargs -r sed -i -E "s#${upstream_re}#${base}#g"
|
||||
|
||||
# What gets installed: the PR, or in an upgrade test the base branch.
|
||||
src_tree="$tree"
|
||||
src_base="$base"
|
||||
if [[ "$TEST_MODE" == "upgrade" ]]; then
|
||||
src_tree="$tree-base"
|
||||
src_base="$base-base"
|
||||
mkdir -p "$src_tree"
|
||||
curl -fsSL --retry 3 "https://codeload.github.com/${UPSTREAM}/tar.gz/${BASE_SHA}" |
|
||||
tar -xz -C "$src_tree" --strip-components=1 || setup_error "could not download ${UPSTREAM}@${BASE_SHA:0:7}"
|
||||
[[ -f "$src_tree/$SCRIPT" ]] || fail "${SCRIPT} does not exist at ${UPSTREAM}@${BASE_SHA:0:7}"
|
||||
grep -rlE "$upstream_re" "$src_tree" --include='*.sh' --include='*.func' |
|
||||
xargs -r sed -i -E "s#${upstream_re}#${src_base}#g"
|
||||
fi
|
||||
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
vm_build_and_check
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Build ----------------------------------------------------------------------
|
||||
if [[ "$TEST_MODE" == "upgrade" ]]; then
|
||||
echo "Upgrade test: installing ${SCRIPT} from ${UPSTREAM}@${BASE_SHA:0:7} (before this PR), then updating with ${PR_REPO}@${PR_SHA:0:7}"
|
||||
fi
|
||||
echo "Building ${SCRIPT} (${OS:-default OS}) on ${PLATFORM} as CT ${ctid} from $([[ "$TEST_MODE" == upgrade ]] && echo "${UPSTREAM}@${BASE_SHA:0:7}" || echo "${PR_REPO}@${PR_SHA:0:7}")"
|
||||
hostname_safe="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-50)"
|
||||
build_env=(
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOME=/root TERM=dumb LANG=C.UTF-8
|
||||
mode=default MODE=default PHS_SILENT=1 DIAGNOSTICS=no
|
||||
var_ctid="$ctid" var_hostname="$hostname_safe" var_brg="$BRIDGE"
|
||||
"dev_mode=timing,keep" COMMUNITY_SCRIPTS_URL="$src_base"
|
||||
)
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
build_env+=(var_container_storage=default)
|
||||
else
|
||||
build_env+=(var_container_storage=local var_template_storage=local)
|
||||
fi
|
||||
touch "$RAW_LOG.start"
|
||||
[[ -n "$OS" ]] && build_env+=(var_os="$OS")
|
||||
[[ -n "$VAR_ARM64" ]] && build_env+=(var_arm64=yes)
|
||||
# Both engines turn var_github_token into GITHUB_TOKEN inside the container.
|
||||
[[ -n "${GH_API_TOKEN:-}" ]] && build_env+=(var_github_token="$GH_API_TOKEN")
|
||||
|
||||
# --- Answers for the script's questions ------------------------------------------
|
||||
# Nobody types into CI, so anything the install asks has to be answered up front.
|
||||
# AGENTS.md rule 24: a script reads var_<x> first and prompts only when it is
|
||||
# unset (or uses prompt_input_required ... "var_x"). Every such variable gets a
|
||||
# CI value here. The names come from json/<app>.json app_vars (DevScripts) and
|
||||
# from the read/prompt_ lines of the install script (both repos). A bare `read`
|
||||
# that ignores its variable still fails, and the report says why.
|
||||
ENGINE_VARS=" var_ctid var_hostname var_os var_version var_cpu var_ram var_disk var_brg var_net var_container_storage var_template_storage var_unprivileged var_tags var_arm64 var_testurl var_verbose var_timezone var_http_proxy var_http_no_proxy var_appversion var_github_token "
|
||||
host_app=$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}")
|
||||
ci_value() {
|
||||
case "${1,,}" in
|
||||
*pass* | *secret* | *token* | *key*) head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | cut -c1-20 ;;
|
||||
*email* | *mail*) echo "ci@example.test" ;;
|
||||
*url* | *endpoint*) echo "https://ci-${host_app}.example.test" ;;
|
||||
# One name per variable: var_opencloud_collabora_fqdn ->
|
||||
# ci-opencloud-collabora.example.test, so several of them cannot collide.
|
||||
*host* | *domain* | *fqdn*)
|
||||
local part="${1#var_}"
|
||||
part=$(sed -E 's/_?(fqdn|hostname|host|domain)$//' <<<"${part,,}" | tr '_' '-')
|
||||
echo "ci-${part:-$host_app}.example.test"
|
||||
;;
|
||||
*user*) echo "ciadmin" ;;
|
||||
*) echo "ci-test" ;;
|
||||
esac
|
||||
}
|
||||
declare -A answers=()
|
||||
json="$src_tree/json/${app}.json"
|
||||
if [[ -f "$json" ]]; then
|
||||
while IFS=$'\t' read -r name default; do
|
||||
answers[$name]="$default"
|
||||
done < <(jq -r '(if type == "array" then .[0] else . end).app_vars // [] | .[] |
|
||||
select(.name | test("^var_[a-z0-9_]{1,40}$")) | [.name, (.default // "" | tostring)] | @tsv' "$json" 2>/dev/null)
|
||||
fi
|
||||
# Names built at run time from a loop, e.g. OpenCloud:
|
||||
# servers=("opencloud" "collabora" "wopi")
|
||||
# for server in "${servers[@]}"; do fqdn_var="var_opencloud_${server}_fqdn"
|
||||
# Each var_...${x}... template is expanded with the words of `for x in ...`
|
||||
# (or of the array that loop walks). Nothing is executed, only read.
|
||||
expand_templates() {
|
||||
local tpl x pre suf words arr w
|
||||
while read -r tpl; do
|
||||
x=$(sed -E 's/^[^$]*\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?.*$/\1/' <<<"$tpl")
|
||||
pre="${tpl%%\$*}"
|
||||
suf=$(sed -E 's/^[^$]*\$\{?[A-Za-z_][A-Za-z0-9_]*\}?//' <<<"$tpl")
|
||||
words=$(grep -m1 -oE "for ${x} in [^;]*" "$1" | sed -E "s/^for ${x} in //")
|
||||
if [[ "$words" =~ \$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\} ]]; then
|
||||
arr="${BASH_REMATCH[1]}"
|
||||
words=$(grep -m1 -oE "^\s*${arr}=\([^)]*\)" "$1" | sed -E "s/^\s*${arr}=\(//; s/\)$//")
|
||||
fi
|
||||
for w in $words; do
|
||||
w="${w//[\"\']/}"
|
||||
[[ "${pre}${w}${suf}" =~ ^var_[a-z0-9_]{1,40}$ ]] && echo "${pre}${w}${suf}"
|
||||
done
|
||||
done < <(grep -oE 'var_[a-z0-9_]*\$\{?[A-Za-z_][A-Za-z0-9_]*\}?[a-z0-9_]*' "$1" | sort -u)
|
||||
}
|
||||
|
||||
# The engine names the install script after APP (lower case, no spaces), which
|
||||
# need not match the ct/ file name: ct/bluesky-pds.sh -> blueskypds-install.sh.
|
||||
nsapp=$(grep -m1 -oE '^APP="[^"]+"' "$src_tree/$SCRIPT" | cut -d'"' -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]')
|
||||
for f in "$src_tree/install/${app}-install.sh" "$src_tree/install/${nsapp}-install.sh" "$src_tree/$SCRIPT"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
while read -r name; do
|
||||
[[ -v "answers[$name]" ]] || answers[$name]=""
|
||||
done < <({
|
||||
grep -E '\bread\b|prompt_(input|input_required|confirm|select|password)\b' "$f" | grep -oE '\bvar_[a-z0-9_]{1,40}\b'
|
||||
expand_templates "$f"
|
||||
} | sort -u)
|
||||
done
|
||||
for name in "${!answers[@]}"; do
|
||||
[[ "$ENGINE_VARS" == *" $name "* ]] && continue
|
||||
value="${answers[$name]}"
|
||||
[[ -n "$value" ]] || value=$(ci_value "$name")
|
||||
build_env+=("$name=$value")
|
||||
case "${name,,}" in
|
||||
*pass* | *secret* | *token* | *key*) echo "Pre-answered ${name}=(random)" ;;
|
||||
*) echo "Pre-answered ${name}=${value}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# In the background so that a TERM from a cancelled job is handled at once:
|
||||
# bash defers traps until a foreground command returns.
|
||||
env -i "${build_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
|
||||
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$src_base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 &
|
||||
build_pid=$!
|
||||
start_streams
|
||||
wait "$build_pid"
|
||||
exit_code=$?
|
||||
build_pid=""
|
||||
stop_streams
|
||||
|
||||
if ((exit_code == 124 || exit_code == 137)); then
|
||||
fail "build timed out after ${BUILD_TIMEOUT}"
|
||||
fi
|
||||
# The engine marks the step that broke with a cross. Its closing "Installation
|
||||
# failed in container" line is generic, so the cross before it is the useful one.
|
||||
failure_line() {
|
||||
local line
|
||||
line=$(strip_ansi <"$RAW_LOG" | grep -E '✖|\[ERROR\]|error:' | grep -v 'Installation failed in container' |
|
||||
tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-200)
|
||||
# A prompt nobody can answer: say what to do instead.
|
||||
# First, because the report keeps 200 characters.
|
||||
if [[ "$line" == *"read exited with status"* || "$line" == *"executing command read "* ]]; then
|
||||
local asked
|
||||
asked=$(grep -oE '\bvar_[a-z0-9_]{1,40}\b' <<<"$line" | tail -n 1)
|
||||
line="unanswerable prompt (AGENTS.md rule 24): ask only when ${asked:-var_<x>} is unset, or use prompt_input_required. ${line}"
|
||||
fi
|
||||
echo "$line"
|
||||
}
|
||||
if ((exit_code != 0)); then
|
||||
last=$(failure_line)
|
||||
step="build exited ${exit_code}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
# With dev_mode=keep (needed to read the install log) the engine carries on
|
||||
# after a failed install and still prints "Completed Successfully", exit 0.
|
||||
install_exit=$(strip_ansi <"$RAW_LOG" | grep -oE 'Installation failed in container [0-9]+ \(exit code: [0-9]+\)' |
|
||||
grep -oE '[0-9]+\)$' | tr -d ')' | tail -n 1)
|
||||
if [[ -n "$install_exit" ]]; then
|
||||
last=$(failure_line)
|
||||
step="install script exited ${install_exit}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
grep -qi 'completed successfully' "$RAW_LOG" || fail "build exited 0 but never reported success"
|
||||
|
||||
# --- Checks ---------------------------------------------------------------------
|
||||
guest_running || fail "container ${ctid} is not running"
|
||||
|
||||
ct_ip=$(guest_exec sh -c "ip -4 -o addr show dev eth0 | awk '{print \$4}' | cut -d/ -f1" 2>/dev/null | head -n1)
|
||||
|
||||
# What the app's page looks like, for the PR comment. Chromium runs as its own
|
||||
# unprivileged user with its sandbox on: the page is served by code under test.
|
||||
# ci-shot-firewall (pve-ci-host-setup.sh) keeps that user off this host and the
|
||||
# lab network; without it there is no screenshot rather than an unguarded one.
|
||||
# $1: file name in OUT_DIR (screenshot.png after install, -update after update).
|
||||
take_screenshot() {
|
||||
command -v chromium >/dev/null && id ci-shot &>/dev/null || return 0
|
||||
iptables -w -S CI_SHOT 2>/dev/null | grep -q -- '-d 10.0.0.0/8 .*REJECT' || return 0
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
chown ci-shot "$dir"
|
||||
# HOME: runuser keeps root's, which ci-shot cannot write; Playwright's
|
||||
# Chromium (the ARM runner) then crashes in its crash handler.
|
||||
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
|
||||
--ignore-certificate-errors --no-first-run --window-size=1280,800 --virtual-time-budget=10000 \
|
||||
--user-data-dir="$dir/profile" --screenshot="$dir/shot.png" "$url" &>/dev/null
|
||||
# Only a plausible PNG leaves this function.
|
||||
if [[ -s "$dir/shot.png" && $(stat -c %s "$dir/shot.png") -lt 3000000 ]] &&
|
||||
[[ "$(head -c 8 "$dir/shot.png" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
|
||||
cp "$dir/shot.png" "$OUT_DIR/${1:-screenshot.png}"
|
||||
fi
|
||||
rm -rf -- "$dir"
|
||||
}
|
||||
|
||||
# Is the app up? No failed services, and the URL the script tells the user to
|
||||
# open, e.g. http://${IP}:1337/admin, answers 200 (after redirects, so a jump
|
||||
# to /login is fine). A script that prints no URL has no web UI to check.
|
||||
# Run after the install and again after the update. Sets health_problem.
|
||||
app_url() { # the URL the script at $1 tells the user to open
|
||||
local pattern
|
||||
pattern=$(grep -oE 'https?://\$\{IP\}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?' "$1" | head -n1)
|
||||
[[ -n "$pattern" && -n "$ct_ip" ]] && echo "${pattern/\$\{IP\}/$ct_ip}"
|
||||
}
|
||||
url=$(app_url "$src_tree/$SCRIPT")
|
||||
health_problem=""
|
||||
check_health() {
|
||||
health_problem=""
|
||||
failed_units=""
|
||||
if guest_exec sh -c 'command -v systemctl' &>/dev/null; then
|
||||
sleep 10
|
||||
failed_units=$(guest_exec systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | paste -sd, -)
|
||||
elif guest_exec sh -c 'command -v rc-status' &>/dev/null; then
|
||||
failed_units=$(guest_exec rc-status --crashed 2>/dev/null | awk '{print $1}' | paste -sd, -)
|
||||
fi
|
||||
[[ -z "$failed_units" ]] || { health_problem="failed services: ${failed_units}"; return 1; }
|
||||
[[ -n "$url" ]] || return 0
|
||||
local waited
|
||||
for ((waited = 0; waited < URL_WAIT; waited += 5)); do
|
||||
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
|
||||
sleep 5
|
||||
done
|
||||
# Also when the check fails: an error page explains more than a status code.
|
||||
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot "$1"
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] ||
|
||||
{ health_problem="${url} answered ${http_code/#000/nothing} instead of 200 within ${URL_WAIT}s"; return 1; }
|
||||
}
|
||||
|
||||
check_health screenshot.png || fail "$health_problem"
|
||||
|
||||
# --- Update -------------------------------------------------------------------------
|
||||
# A fresh install is already on the latest release, so a plain `update` would
|
||||
# only say "No update available" and never run update_script's real path. The
|
||||
# version files the install wrote (~/.<app>, one per fetch_and_deploy_gh_release)
|
||||
# that update_script checks are set back to 0.0.0-ci first; check_for_gh_release then sees an update and
|
||||
# runs it all. Apps deployed from a branch (fetch_and_deploy_gh_branch, checked
|
||||
# with check_for_gh_branch) store a commit hash instead of a version; the
|
||||
# engine compares both as plain strings, so 0.0.0-ci works for them too. `update` is the /usr/bin/update the install generated: it pulls
|
||||
# ct/<app>.sh from the URL this container was built from, i.e. the PR's tree.
|
||||
UPDATE_FAKE="0.0.0-ci"
|
||||
update_fail() {
|
||||
update_status="fail"
|
||||
update_step="$1"
|
||||
fail "update: $1"
|
||||
}
|
||||
|
||||
version_files=$(guest_exec sh -c '
|
||||
for f in /root/.*; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in */.install-* | *.pending | *.bak | */.bash* | */.profile | */.hushlogin | */.viminfo | */.lesshst | */.wget-hsts | */.selected_editor) continue ;; esac
|
||||
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^(v?[0-9]+(\.[0-9A-Za-z_+-]+)+|[0-9a-f]{7,40})$" "$f" && echo "$f"
|
||||
done' 2>/dev/null | grep -E '^/root/\.[A-Za-z0-9._-]+$')
|
||||
# Only set back what update_script checks: the names it passes to
|
||||
# check_for_*release. The install can also deploy helpers the update never
|
||||
# touches (immich: ~/.mise); those would stay 0.0.0-ci and fail the run. A
|
||||
# name that is a variable can't be read here, so then all of them are set back.
|
||||
tracked=$(grep -oE 'check_for_[a-z]*_?release[[:space:]]+("[^"]*"|[^[:space:]]+)' "$tree/$SCRIPT" |
|
||||
sed -E 's/^check_for_[a-z]*_?release[[:space:]]+//; s/^"(.*)"$/\1/' | tr -d ' ' | tr '[:upper:]' '[:lower:]' | sort -u)
|
||||
if [[ -n "$tracked" && "$tracked" != *'$'* ]]; then
|
||||
version_files=$(grep -xF -f <(sed 's|^|/root/.|' <<<"$tracked") <<<"$version_files" || true)
|
||||
fi
|
||||
for f in $version_files; do
|
||||
guest_exec sh -c "echo '$UPDATE_FAKE' > '$f'; rm -f '$f.pending'" ||
|
||||
update_fail "could not reset $f"
|
||||
done
|
||||
[[ -n "$version_files" ]] && echo "Update test: set back $(tr '\n' ' ' <<<"$version_files")to ${UPDATE_FAKE}"
|
||||
|
||||
phase="update"
|
||||
# Upgrade test: the container pulls its update from where it was installed
|
||||
# from (COMMUNITY_SCRIPTS_URL in /usr/bin/update); point that at the PR, as
|
||||
# merging it would.
|
||||
if [[ "$TEST_MODE" == "upgrade" ]]; then
|
||||
guest_exec sed -i -E "s#^export COMMUNITY_SCRIPTS_URL=.*#export COMMUNITY_SCRIPTS_URL=\"${base}\"#" /usr/bin/update &&
|
||||
guest_exec grep -qxF "export COMMUNITY_SCRIPTS_URL=\"${base}\"" /usr/bin/update ||
|
||||
setup_error "could not point /usr/bin/update at the PR"
|
||||
echo "Upgrade test: /usr/bin/update now pulls from ${PR_REPO}@${PR_SHA:0:7}"
|
||||
fi
|
||||
|
||||
# run_update <raw log> <title>: runs `update` in the container, live in the
|
||||
# job log; sets update_rc.
|
||||
run_update() {
|
||||
local raw=$1 token
|
||||
: >"$raw"
|
||||
token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
echo "──────── $2 (live) ────────"
|
||||
echo "::stop-commands::${token}"
|
||||
# timeout runs a program, not a shell function, so spell out the exec here.
|
||||
if [[ "$PLATFORM" == "incus" ]]; then guest_cmd=(incus exec "$ctid" --); else guest_cmd=(pct exec "$ctid" --); fi
|
||||
timeout --kill-after=1m "$UPDATE_TIMEOUT" "${guest_cmd[@]}" env PHS_SILENT=1 mode=default MODE=default \
|
||||
TERM=dumb DIAGNOSTICS=no var_ignore_breaking_changes=yes ${GH_API_TOKEN:+GITHUB_TOKEN="$GH_API_TOKEN"} update </dev/null \
|
||||
> >(tee "$raw" | live_filter) 2>&1 &
|
||||
update_pid=$!
|
||||
wait "$update_pid"
|
||||
update_rc=$?
|
||||
update_pid=""
|
||||
sleep 1
|
||||
echo "::${token}::"
|
||||
}
|
||||
run_update "$UPDATE_RAW" "update test"
|
||||
|
||||
update_failure_line() {
|
||||
strip_ansi <"${1:-$UPDATE_RAW}" | grep -E '✖|\[ERROR\]|error:' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180
|
||||
}
|
||||
if ((update_rc == 124 || update_rc == 137)); then
|
||||
update_fail "timed out after ${UPDATE_TIMEOUT}"
|
||||
elif ((update_rc != 0)); then
|
||||
last=$(update_failure_line)
|
||||
update_fail "exited ${update_rc}${last:+: $last}"
|
||||
fi
|
||||
# The engine can report an error and still exit 0 (bark-server: "github: rate
|
||||
# limit reached", then "Cleaned"). A rate limit is ours, not the PR's: the
|
||||
# update could not be tested. Any other error line is a failed update.
|
||||
update_error=$(strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
|
||||
if [[ -n "$update_error" ]]; then
|
||||
if [[ "${update_error,,}" == *"rate limit"* ]]; then
|
||||
update_status="error"
|
||||
update_step="could not be tested: ${update_error}"
|
||||
status="pass"
|
||||
finish
|
||||
exit 0
|
||||
fi
|
||||
update_fail "$update_error"
|
||||
fi
|
||||
if strip_ansi <"$UPDATE_RAW" | grep -qi 'updated successfully'; then
|
||||
update_status="updated"
|
||||
for f in $version_files; do
|
||||
[[ "$(guest_exec cat "$f" 2>/dev/null)" != "$UPDATE_FAKE" ]] ||
|
||||
update_fail "reported success, but $f is still ${UPDATE_FAKE}"
|
||||
done
|
||||
else
|
||||
# Ran cleanly without updating anything: no version file to fake (apt/npm
|
||||
# based), or an update_script that decides differently. Not a failure.
|
||||
update_status="no-update"
|
||||
update_step=$(strip_ansi <"$UPDATE_RAW" | grep -iE 'no update|up to date|up-to-date|already' | tail -n 1 | sed -E 's/^\s*(✔️?|ℹ️?|\s)*//' | cut -c1-160)
|
||||
fi
|
||||
|
||||
url=$(app_url "$tree/$SCRIPT")
|
||||
check_health screenshot-update.png || update_fail "after the update: $health_problem"
|
||||
|
||||
# --- Second update ------------------------------------------------------------------
|
||||
# Right after an update, `update` has nothing to do: it must say so and leave
|
||||
# the app alone. One that breaks on a second run breaks for every user on
|
||||
# their next update; one that updates again although nothing changed
|
||||
# rebuilds on every run (often a version file it never writes).
|
||||
if [[ "$update_status" == "updated" ]]; then
|
||||
run_update "$UPDATE2_RAW" "second update (must have nothing to do)"
|
||||
if ((update_rc == 124 || update_rc == 137)); then
|
||||
update_fail "second run timed out after ${UPDATE_TIMEOUT}"
|
||||
elif ((update_rc != 0)); then
|
||||
last=$(update_failure_line "$UPDATE2_RAW")
|
||||
update_fail "second run exited ${update_rc}${last:+: $last}"
|
||||
fi
|
||||
update_error=$(strip_ansi <"$UPDATE2_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
|
||||
if [[ -n "$update_error" && "${update_error,,}" != *"rate limit"* ]]; then
|
||||
update_fail "second run: $update_error"
|
||||
fi
|
||||
if strip_ansi <"$UPDATE2_RAW" | grep -qi 'updated successfully'; then
|
||||
update2_status="updated-again"
|
||||
notes+=("a second \`update\` right after the first updated again: update_script does not notice it is current (e.g. a version file it never writes)")
|
||||
else
|
||||
update2_status="no-update"
|
||||
fi
|
||||
check_health screenshot-update.png || update_fail "after the second update: $health_problem"
|
||||
fi
|
||||
|
||||
# --- Does it stay up? -----------------------------------------------------------
|
||||
# A service that crash-loops can be "active" at every single look. Count the
|
||||
# restarts of every service (and Docker container) over STABLE_WAIT seconds:
|
||||
# any that went up was restarted while nobody touched the container.
|
||||
restart_counts() {
|
||||
guest_exec sh -c '
|
||||
if command -v systemctl >/dev/null; then
|
||||
systemctl list-units --type=service --all --no-legend --plain | awk "{print \$1}" |
|
||||
while read -r u; do echo "$u $(systemctl show -p NRestarts --value "$u" 2>/dev/null)"; done
|
||||
fi
|
||||
if command -v docker >/dev/null; then
|
||||
docker ps -aq | xargs -r docker inspect -f "docker:{{.Name}} {{.RestartCount}}" 2>/dev/null
|
||||
fi' 2>/dev/null | awk '$2 ~ /^[0-9]+$/' | sort
|
||||
}
|
||||
before=$(restart_counts)
|
||||
sleep "$STABLE_WAIT"
|
||||
after=$(restart_counts)
|
||||
restarting=$(join <(echo "$before") <(echo "$after") | awk '$3 > $2 {printf "%s%s (+%d)", sep, $1, $3 - $2; sep=", "}')
|
||||
[[ -z "$restarting" ]] || fail "not stable: restarted within ${STABLE_WAIT}s without anyone touching it: ${restarting}"
|
||||
check_health screenshot-update.png || fail "not stable: ${STABLE_WAIT}s later, ${health_problem}"
|
||||
|
||||
# Error lines in the journal since boot: often harmless, sometimes the reason
|
||||
# a feature is broken. Information only.
|
||||
if guest_exec sh -c 'command -v journalctl' &>/dev/null; then
|
||||
# Without the noise every container produces: udev may not trigger devices
|
||||
# in an unprivileged container, and logind finds no VTs.
|
||||
journal_errors=$(guest_exec journalctl -b -p err -q -o cat --no-pager 2>/dev/null | grep -vE '^\s*$' |
|
||||
grep -vE "Failed to write 'add' to '/sys/.*/uevent'|Seat has no VTs but VT number not 0" | sort | uniq -c | sort -rn)
|
||||
if [[ -n "$journal_errors" ]]; then
|
||||
n=$(awk '{s += $1} END {print s}' <<<"$journal_errors")
|
||||
top=$(head -n 2 <<<"$journal_errors" | sed -E 's/^\s*[0-9]+\s+//' | cut -c1-120 | paste -sd'|' - | sed 's/|/; /g')
|
||||
notes+=("${n} error line(s) in the journal since boot, e.g. ${top}")
|
||||
fi
|
||||
fi
|
||||
|
||||
status="pass"
|
||||
finish
|
||||
Generated
+140
@@ -0,0 +1,140 @@
|
||||
# shellcheck shell=bash
|
||||
# The vm/ half of pve-run-script.sh, sourced from it: same inputs, same
|
||||
# result.json, same live output. Only building and checking differ.
|
||||
#
|
||||
# A VM script runs entirely on this host (there is no install script inside
|
||||
# the guest to follow), unattended through the core engine's VM_UNATTENDED=1:
|
||||
# the script's defaults, with ID, name, bridge and storage set from here. The
|
||||
# guest then boots on vmbr1 like the containers do, so its address is whatever
|
||||
# this host's dnsmasq leased to its MAC; no guest agent needed.
|
||||
#
|
||||
# What passes: the script exits 0 and says "Completed successfully", the VM is
|
||||
# running and keeps running through the boot wait, and where the script prints
|
||||
# a URL built from the VM's address (http://${VM_IP}:9000) that URL answers 200.
|
||||
# An installer ISO or an image without DHCP gets no address; that is reported,
|
||||
# not failed. There is no update test: a VM has no `update`.
|
||||
|
||||
VM_BOOT_WAIT="${VM_BOOT_WAIT:-300}"
|
||||
VM_URL_WAIT="${VM_URL_WAIT:-600}"
|
||||
LEASES="${LEASES:-/var/lib/misc/dnsmasq.leases}"
|
||||
|
||||
# One VM build per host at a time: a VM takes 2-8 GB of RAM and a disk image,
|
||||
# where a container takes a fraction. Held until the job exits; the build is
|
||||
# started with this descriptor closed, so a QEMU process can never inherit it.
|
||||
vm_wait_turn() {
|
||||
exec 7>/run/ci-jobs/.vm-turn
|
||||
flock -n 7 && return 0
|
||||
echo "Another VM build is running on this host; waiting for it (one VM at a time)"
|
||||
flock -w 3600 7 || setup_error "waited an hour for this host's VM slot"
|
||||
}
|
||||
|
||||
vm_cleanup() {
|
||||
if qm status "$ctid" &>/dev/null; then
|
||||
qm stop "$ctid" --skiplock 1 --timeout 30 &>/dev/null || true
|
||||
qm destroy "$ctid" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null || true
|
||||
fi
|
||||
rm -f "/run/ci-jobs/$ctid" "/run/ci-jobs/console-${ctid}.png"
|
||||
# Cloud-Init network snippets the script wrote for this VM.
|
||||
[[ -f "$RAW_LOG.start" ]] &&
|
||||
find /var/lib/vz/snippets -maxdepth 1 -type f -name "*${ctid}*" -newer "$RAW_LOG.start" -delete 2>/dev/null
|
||||
true
|
||||
}
|
||||
|
||||
# The VM's screen, straight from QEMU. Pixels only, but they come from the
|
||||
# guest, so the same checks as for a browser screenshot apply.
|
||||
vm_console_shot() {
|
||||
local f="/run/ci-jobs/console-${ctid}.png"
|
||||
rm -f "$f"
|
||||
printf 'screendump %s -f png\n' "$f" | timeout 30 qm monitor "$ctid" &>/dev/null
|
||||
if [[ -s "$f" && $(stat -c %s "$f") -lt 3000000 ]] &&
|
||||
[[ "$(head -c 8 "$f" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
|
||||
cp "$f" "$OUT_DIR/screenshot.png"
|
||||
fi
|
||||
rm -f "$f"
|
||||
}
|
||||
|
||||
vm_running() {
|
||||
qm status "$ctid" 2>/dev/null | grep -q 'status: running'
|
||||
}
|
||||
|
||||
vm_build_and_check() {
|
||||
local hn mac pattern waited last
|
||||
hn="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-40)-${ctid}"
|
||||
echo "Building ${SCRIPT} as VM ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
|
||||
local vm_env=(
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOME=/root TERM=dumb LANG=C.UTF-8 DIAGNOSTICS=no
|
||||
VM_UNATTENDED=1 VM_VMID="$ctid" VM_HOSTNAME="$hn" VM_BRIDGE="$BRIDGE"
|
||||
VM_STORAGE=local VM_ISO_STORAGE=local VM_START=yes
|
||||
COMMUNITY_SCRIPTS_URL="$base"
|
||||
)
|
||||
# Release lookups (vm_latest_from_github and the like).
|
||||
[[ -n "${GH_API_TOKEN:-}" ]] && vm_env+=(GITHUB_TOKEN="$GH_API_TOKEN")
|
||||
touch "$RAW_LOG.start"
|
||||
|
||||
env -i "${vm_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
|
||||
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 7>&- &
|
||||
build_pid=$!
|
||||
start_streams
|
||||
wait "$build_pid"
|
||||
exit_code=$?
|
||||
build_pid=""
|
||||
stop_streams
|
||||
|
||||
((exit_code == 124 || exit_code == 137)) && fail "build timed out after ${BUILD_TIMEOUT}"
|
||||
if ((exit_code != 0)); then
|
||||
last=$(failure_line)
|
||||
step="script exited ${exit_code}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
# vm_finish ends on a msg_ok of the script's choosing ("VM created. ..."),
|
||||
# older scripts on "Completed successfully". exit_script and friends leave
|
||||
# with 0 too, but on a cross.
|
||||
last=$(strip_ansi <"$RAW_LOG" | tail -n 1 | cut -c1-200)
|
||||
if ! grep -qi 'completed successfully' "$RAW_LOG" && ! [[ "$last" =~ ^[[:space:]]*✔ ]]; then
|
||||
fail "script exited 0 but never reported success${last:+ (last line: ${last})}"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} is not running after the script finished"
|
||||
|
||||
# The address dnsmasq handed to this VM's MAC. The VM must stay up while it
|
||||
# boots: one that powers itself off or crashes is a failure.
|
||||
mac=$(qm config "$ctid" 2>/dev/null | sed -nE 's/^net0:[^=]*=([0-9A-Fa-f:]{17}).*/\1/p' | tr 'A-F' 'a-f')
|
||||
echo "Waiting up to ${VM_BOOT_WAIT}s for VM ${ctid} (${mac:-no NIC}) to boot and get an address"
|
||||
for ((waited = 0; waited < VM_BOOT_WAIT; waited += 5)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while booting"
|
||||
[[ -n "$mac" ]] && ct_ip=$(awk -v m="$mac" '$2 == m {print $3}' "$LEASES" 2>/dev/null | tail -n 1)
|
||||
[[ -n "$ct_ip" ]] && break
|
||||
sleep 5
|
||||
done
|
||||
[[ -n "$ct_ip" ]] && echo "VM ${ctid} got ${ct_ip}" || echo "VM ${ctid} got no DHCP lease within ${VM_BOOT_WAIT}s"
|
||||
|
||||
# Only a URL the script builds from the address it found; <VM-IP> in a
|
||||
# summary is a hint for later (after an installer ran), not a promise.
|
||||
# ${VM_IP}, $VM_IP and ${VM_IP:-<VM-IP>} all count.
|
||||
local ip_re='\$\{?VM_IP(:-[^}]*)?\}?'
|
||||
pattern=$(grep -oE "https?://${ip_re}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?" "$tree/$SCRIPT" | head -n1)
|
||||
if [[ -n "$pattern" ]]; then
|
||||
[[ -n "$ct_ip" ]] || fail "no IP address within ${VM_BOOT_WAIT}s, so ${pattern} could not be checked"
|
||||
url=$(sed -E "s#${ip_re}#${ct_ip}#" <<<"$pattern")
|
||||
for ((waited = 0; waited < VM_URL_WAIT; waited += 10)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while its web UI was awaited"
|
||||
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
|
||||
sleep 10
|
||||
done
|
||||
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot screenshot.png
|
||||
else
|
||||
# Let the console settle on a login prompt rather than a boot message.
|
||||
sleep 20
|
||||
fi
|
||||
[[ -s "$OUT_DIR/screenshot.png" ]] || vm_console_shot
|
||||
if [[ -n "$url" && ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
fail "${url} answered ${http_code/#000/nothing} instead of 200 within ${VM_URL_WAIT}s"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} stopped by itself"
|
||||
|
||||
status="pass"
|
||||
finish
|
||||
}
|
||||
+165
-186
@@ -125,6 +125,9 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -137,6 +140,13 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
<summary><h3>2026</h3></summary>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><h4>November (7 entries)</h4></summary>
|
||||
|
||||
[View November 2026 Changelog](.github/changelogs/2026/11.md)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><h4>October (31 entries)</h4></summary>
|
||||
|
||||
@@ -571,6 +581,160 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-11-12
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Pixelfed ([#18523](https://github.com/community-scripts/ProxmoxVE/pull/18523))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Whisparr-eros: set the first login and save it [@MickLesk](https://github.com/MickLesk) ([#18617](https://github.com/community-scripts/ProxmoxVE/pull/18617))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Updated ASP.NET Web App script to Debian and adds a .Net selection [@kris701](https://github.com/kris701) ([#18628](https://github.com/community-scripts/ProxmoxVE/pull/18628))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- pocketbase-bot: apply website metadata requests from Accept/Decline boxes [@MickLesk](https://github.com/MickLesk) ([#18631](https://github.com/community-scripts/ProxmoxVE/pull/18631))
|
||||
|
||||
## 2026-11-11
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- TrueNas: Fixing isolookup and dialog [@juronja](https://github.com/juronja) ([#18578](https://github.com/community-scripts/ProxmoxVE/pull/18578))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich: bump to 3.4.1 [@MickLesk](https://github.com/MickLesk) ([#18619](https://github.com/community-scripts/ProxmoxVE/pull/18619))
|
||||
- Immich: bump to 3.4.0 [@MickLesk](https://github.com/MickLesk) ([#18600](https://github.com/community-scripts/ProxmoxVE/pull/18600))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Pocketbase: install and update from the new host (removed in github) [@MickLesk](https://github.com/MickLesk) ([#18609](https://github.com/community-scripts/ProxmoxVE/pull/18609))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Paperless-AI: install and update to 4.0 [@MickLesk](https://github.com/MickLesk) ([#18620](https://github.com/community-scripts/ProxmoxVE/pull/18620))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- core: drop the SUDO_ variables an update through sudo carries [@MickLesk](https://github.com/MickLesk) ([core#229](https://github.com/community-scripts/core/pull/229))
|
||||
- feat: let setup_deb822_repo use a keyring already on the system [@MickLesk](https://github.com/MickLesk) ([core#227](https://github.com/community-scripts/core/pull/227))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Fill platforms when a script moves from DevScripts [@MickLesk](https://github.com/MickLesk) ([#18611](https://github.com/community-scripts/ProxmoxVE/pull/18611))
|
||||
|
||||
## 2026-11-10
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- writefreely: Force symbolic link update [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18588](https://github.com/community-scripts/ProxmoxVE/pull/18588))
|
||||
- openproject: start memcached when IPv6 is disabled [@MickLesk](https://github.com/MickLesk) ([#18584](https://github.com/community-scripts/ProxmoxVE/pull/18584))
|
||||
- homeassistant.sh: Update pip install command for runlike [@Zajsoft](https://github.com/Zajsoft) ([#18583](https://github.com/community-scripts/ProxmoxVE/pull/18583))
|
||||
- Tunarr: fix install and update for the new release layout and latest ErsatzTV-ffmpeg [@chrisbenincasa](https://github.com/chrisbenincasa) ([#18577](https://github.com/community-scripts/ProxmoxVE/pull/18577))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: keep a script's config file when a package installs it [@MickLesk](https://github.com/MickLesk) ([core#226](https://github.com/community-scripts/core/pull/226))
|
||||
- fix: flush the image before importing a VM disk [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([core#223](https://github.com/community-scripts/core/pull/223))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- pr-script-test: pre-answer prompt variables built in a loop [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18596](https://github.com/community-scripts/ProxmoxVE/pull/18596))
|
||||
- Build PR scripts on a Proxmox test host via /test [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18594](https://github.com/community-scripts/ProxmoxVE/pull/18594))
|
||||
|
||||
## 2026-11-09
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- zipline: Update RAM to 3GB [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18569](https://github.com/community-scripts/ProxmoxVE/pull/18569))
|
||||
- OpenObserve: record installed version, drop killall ExecStop [@BadFlo](https://github.com/BadFlo) ([#18566](https://github.com/community-scripts/ProxmoxVE/pull/18566))
|
||||
- OpenObserve: extract release tarball with --no-same-owner [@BadFlo](https://github.com/BadFlo) ([#18564](https://github.com/community-scripts/ProxmoxVE/pull/18564))
|
||||
- Immich: download before stopping, and clear maintenance mode after an interrupted update [@angusmaul](https://github.com/angusmaul) ([#18553](https://github.com/community-scripts/ProxmoxVE/pull/18553))
|
||||
- cinephage: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18374](https://github.com/community-scripts/ProxmoxVE/pull/18374))
|
||||
- kan: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18549](https://github.com/community-scripts/ProxmoxVE/pull/18549))
|
||||
- myip: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18550](https://github.com/community-scripts/ProxmoxVE/pull/18550))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- security: remove hardcoded credentials in several scripts [@MickLesk](https://github.com/MickLesk) ([#18572](https://github.com/community-scripts/ProxmoxVE/pull/18572))
|
||||
- yuvomi: install from the attested release asset [@ulsklyc](https://github.com/ulsklyc) ([#18554](https://github.com/community-scripts/ProxmoxVE/pull/18554))
|
||||
|
||||
### 🗑️ Deleted Scripts
|
||||
|
||||
- coolify: delete old ct / its an addon [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18562](https://github.com/community-scripts/ProxmoxVE/pull/18562))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- core: detect GPUs by their PCI class, not by text anywhere in the line [@MickLesk](https://github.com/MickLesk) ([core#221](https://github.com/community-scripts/core/pull/221))
|
||||
- core: load an app's own settings from its .vars file [@MickLesk](https://github.com/MickLesk) ([core#216](https://github.com/community-scripts/core/pull/216))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- IP-Tag: filter guest-agent IPs by CIDR before limiting to 3 [@mazelado](https://github.com/mazelado) ([#18537](https://github.com/community-scripts/ProxmoxVE/pull/18537))
|
||||
|
||||
## 2026-11-08
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Chatto ([#18521](https://github.com/community-scripts/ProxmoxVE/pull/18521))
|
||||
- Firefox-Syncserver ([#18520](https://github.com/community-scripts/ProxmoxVE/pull/18520))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Immich: do not clone libjxl's testdata [@MickLesk](https://github.com/MickLesk) ([#18539](https://github.com/community-scripts/ProxmoxVE/pull/18539))
|
||||
- Databasus: build 3.65 and ship the verification agent [@MickLesk](https://github.com/MickLesk) ([#18500](https://github.com/community-scripts/ProxmoxVE/pull/18500))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: a closed console no longer ends a running setup | ERR trap fixes | check VM ID before creating [@MickLesk](https://github.com/MickLesk) ([core#219](https://github.com/community-scripts/core/pull/219))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- update-apps: keep the backup alive on a closed console, stop before restoring [@MickLesk](https://github.com/MickLesk) ([#18540](https://github.com/community-scripts/ProxmoxVE/pull/18540))
|
||||
- clean-lxcs: run with sh and keep going when one container fails [@MickLesk](https://github.com/MickLesk) ([#18538](https://github.com/community-scripts/ProxmoxVE/pull/18538))
|
||||
- update-lxcs: keep going when one container fails [@MickLesk](https://github.com/MickLesk) ([#18541](https://github.com/community-scripts/ProxmoxVE/pull/18541))
|
||||
|
||||
## 2026-11-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
|
||||
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
|
||||
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
|
||||
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
|
||||
|
||||
## 2026-11-06
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
@@ -1319,189 +1483,4 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- FileBrowser Quantum: migrate the config and database to v2 [@MickLesk](https://github.com/MickLesk) ([#17815](https://github.com/community-scripts/ProxmoxVE/pull/17815))
|
||||
|
||||
## 2026-10-08
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Unifi-OS-Server VM ([#17752](https://github.com/community-scripts/ProxmoxVE/pull/17752))
|
||||
- ZimaOS VM ([#17778](https://github.com/community-scripts/ProxmoxVE/pull/17778))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Revert "Immich: Pin to v3.3.0" [@MickLesk](https://github.com/MickLesk) ([#17767](https://github.com/community-scripts/ProxmoxVE/pull/17767))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Homarr: replace the musl better-sqlite3 that v2.3.0 ships for Debian [@MickLesk](https://github.com/MickLesk) ([#17789](https://github.com/community-scripts/ProxmoxVE/pull/17789))
|
||||
- CLIProxyAPI: keep plugins and data across updates [@MickLesk](https://github.com/MickLesk) ([#17790](https://github.com/community-scripts/ProxmoxVE/pull/17790))
|
||||
- Kima-Hub: install the Python services into a uv venv [@MickLesk](https://github.com/MickLesk) ([#17791](https://github.com/community-scripts/ProxmoxVE/pull/17791))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich: Pin to 3.3.0 / Update ML Python to 3.13 [@MickLesk](https://github.com/MickLesk) ([#17770](https://github.com/community-scripts/ProxmoxVE/pull/17770))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: OPNsense VM [@MickLesk](https://github.com/MickLesk) ([#17785](https://github.com/community-scripts/ProxmoxVE/pull/17785))
|
||||
- Refactor: Nextcloud VM (Turnkey) [@MickLesk](https://github.com/MickLesk) ([#17787](https://github.com/community-scripts/ProxmoxVE/pull/17787))
|
||||
- Refactor: OpenWrt VM [@MickLesk](https://github.com/MickLesk) ([#17774](https://github.com/community-scripts/ProxmoxVE/pull/17774))
|
||||
- Refactor: Ubuntu VM [@MickLesk](https://github.com/MickLesk) ([#17776](https://github.com/community-scripts/ProxmoxVE/pull/17776))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Tolerate a missing datacenter.cfg in vm_keyboard_default [@MickLesk](https://github.com/MickLesk) ([core#124](https://github.com/community-scripts/core/pull/124))
|
||||
- Take the ISO storage from the disk pool when it can hold ISOs [@MickLesk](https://github.com/MickLesk) ([core#122](https://github.com/community-scripts/core/pull/122))
|
||||
- Shared VM helpers: disk import, releases, checksums, first boot, IP by MAC [@MickLesk](https://github.com/MickLesk) ([core#117](https://github.com/community-scripts/core/pull/117))
|
||||
- tools: forge truncated release list [@MickLesk](https://github.com/MickLesk) ([core#118](https://github.com/community-scripts/core/pull/118))
|
||||
- Remember a kept cached image until the upstream changes [@MickLesk](https://github.com/MickLesk) ([core#119](https://github.com/community-scripts/core/pull/119))
|
||||
- Choose the VM keyboard layout and carry the host's timezone into prepared images [@MickLesk](https://github.com/MickLesk) ([core#120](https://github.com/community-scripts/core/pull/120))
|
||||
- VM's: run first-boot units without debconf dialogs [@MickLesk](https://github.com/MickLesk) ([core#121](https://github.com/community-scripts/core/pull/121))
|
||||
|
||||
## 2026-10-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Fedora VM ([#17747](https://github.com/community-scripts/ProxmoxVE/pull/17747))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- AudioMuse-AI: replace the venv on update without asking [@MickLesk](https://github.com/MickLesk) ([#17738](https://github.com/community-scripts/ProxmoxVE/pull/17738))
|
||||
- paperclip: run as a dedicated non-root user so Claude Code can skip permissions [@samvandenbossche](https://github.com/samvandenbossche) ([#17707](https://github.com/community-scripts/ProxmoxVE/pull/17707))
|
||||
- Thingsboard: update Java version from 17 to 25 [@CerberusStyle](https://github.com/CerberusStyle) ([#17733](https://github.com/community-scripts/ProxmoxVE/pull/17733))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich: Pin to v3.3.0 [@vhsdream](https://github.com/vhsdream) ([#17759](https://github.com/community-scripts/ProxmoxVE/pull/17759))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- OpenCloud: bump to v8.1.0, rebuild search index on upgrade [@SimKaiLong](https://github.com/SimKaiLong) ([#17710](https://github.com/community-scripts/ProxmoxVE/pull/17710))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Pangolin: Unpin Release, stable enough, Bump to latest [@MickLesk](https://github.com/MickLesk) ([#17740](https://github.com/community-scripts/ProxmoxVE/pull/17740))
|
||||
- Refactor: Archlinux-VM [@MickLesk](https://github.com/MickLesk) ([#17741](https://github.com/community-scripts/ProxmoxVE/pull/17741))
|
||||
- Refactor: MikroTik RouterOS [@MickLesk](https://github.com/MickLesk) ([#17748](https://github.com/community-scripts/ProxmoxVE/pull/17748))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Fix Fedora and BLS cloud image console setup [@MickLesk](https://github.com/MickLesk) ([core#116](https://github.com/community-scripts/core/pull/116))
|
||||
- Keep setup_nodejs alive when the caller's directory is gone [@MickLesk](https://github.com/MickLesk) ([core#114](https://github.com/community-scripts/core/pull/114))
|
||||
|
||||
## 2026-10-06
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Pricebuddy ([#17708](https://github.com/community-scripts/ProxmoxVE/pull/17708))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Webtrees: stop serving data/ and the source folders directly [@MickLesk](https://github.com/MickLesk) ([#17724](https://github.com/community-scripts/ProxmoxVE/pull/17724))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Incus: check the architecture on Incus hosts too [@MickLesk](https://github.com/MickLesk) ([core#113](https://github.com/community-scripts/core/pull/113))
|
||||
- Incus: Pass the app's var_* settings into Incus containers [@MickLesk](https://github.com/MickLesk) ([core#112](https://github.com/community-scripts/core/pull/112))
|
||||
- Incus: stub storage_content_check on Incus [@MickLesk](https://github.com/MickLesk) ([core#104](https://github.com/community-scripts/core/pull/104))
|
||||
- Incus: map Proxmox template versions to Incus image names [@MickLesk](https://github.com/MickLesk) ([core#111](https://github.com/community-scripts/core/pull/111))
|
||||
- Incus: read storage names from .vars files [@MickLesk](https://github.com/MickLesk) ([core#110](https://github.com/community-scripts/core/pull/110))
|
||||
- Incus: show the spinner inside containers again [@MickLesk](https://github.com/MickLesk) ([core#109](https://github.com/community-scripts/core/pull/109))
|
||||
- Incus: set the hostname with sh, so it works before bash is installed [@MickLesk](https://github.com/MickLesk) ([core#102](https://github.com/community-scripts/core/pull/102))
|
||||
- Incus: reserve a plain address on Incus, leave the gateway to the network [@MickLesk](https://github.com/MickLesk) ([core#101](https://github.com/community-scripts/core/pull/101))
|
||||
- Incus: Wait for the network on Alpine OS too [@MickLesk](https://github.com/MickLesk) ([core#105](https://github.com/community-scripts/core/pull/105))
|
||||
- Incus: do not fail the build when hostname -I is missing [@MickLesk](https://github.com/MickLesk) ([core#103](https://github.com/community-scripts/core/pull/103))
|
||||
- Incus: Fuse always in unprivileged Containers, attach TUN only when container lacks it [@MickLesk](https://github.com/MickLesk) ([core#108](https://github.com/community-scripts/core/pull/108))
|
||||
- Incus: accept mode=generated [@MickLesk](https://github.com/MickLesk) ([core#106](https://github.com/community-scripts/core/pull/106))
|
||||
- Incus: pass the full install environment into Incus containers (quoted) [@MickLesk](https://github.com/MickLesk) ([core#107](https://github.com/community-scripts/core/pull/107))
|
||||
|
||||
## 2026-10-05
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Point to DevScripts, the renamed ProxmoxVED [@MickLesk](https://github.com/MickLesk) ([#17694](https://github.com/community-scripts/ProxmoxVE/pull/17694))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- wanderer: set the proxy secret and install plugins in their own directories [@MickLesk](https://github.com/MickLesk) ([#17698](https://github.com/community-scripts/ProxmoxVE/pull/17698))
|
||||
- discourse: serve stylesheets and repair the update [@MickLesk](https://github.com/MickLesk) ([#17697](https://github.com/community-scripts/ProxmoxVE/pull/17697))
|
||||
- wikijs: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17689](https://github.com/community-scripts/ProxmoxVE/pull/17689))
|
||||
- jotty: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17688](https://github.com/community-scripts/ProxmoxVE/pull/17688))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Read releases from github.com when the API is rate limited, resume stalled downloads [@MickLesk](https://github.com/MickLesk) ([core#99](https://github.com/community-scripts/core/pull/99))
|
||||
- Check for template and container storage before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#98](https://github.com/community-scripts/core/pull/98))
|
||||
- Check /etc/pve before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#97](https://github.com/community-scripts/core/pull/97))
|
||||
- Check container settings before pct create rejects them [@MickLesk](https://github.com/MickLesk) ([core#96](https://github.com/community-scripts/core/pull/96))
|
||||
- PVE: Smart Diagnosis - say why a container would not start [@MickLesk](https://github.com/MickLesk) ([core#95](https://github.com/community-scripts/core/pull/95))
|
||||
- Retry template downloads that pveam reports as done but are not [@MickLesk](https://github.com/MickLesk) ([core#94](https://github.com/community-scripts/core/pull/94))
|
||||
- Check the OS release against pve-container before creating anything [@MickLesk](https://github.com/MickLesk) ([core#93](https://github.com/community-scripts/core/pull/93))
|
||||
- Rename "ProxmoxVED"-Links to "DevScripts" [@MickLesk](https://github.com/MickLesk) ([core#91](https://github.com/community-scripts/core/pull/91))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- monitor-all: read container IPs from the host side [@jasonobrien](https://github.com/jasonobrien) ([#17686](https://github.com/community-scripts/ProxmoxVE/pull/17686))
|
||||
|
||||
## 2026-10-04
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- immich: read the Intel runtime from the pinned release [@MickLesk](https://github.com/MickLesk) ([#17677](https://github.com/community-scripts/ProxmoxVE/pull/17677))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- better explain unsupported version & upgrade path for pve [@MickLesk](https://github.com/MickLesk) ([core#86](https://github.com/community-scripts/core/pull/86))
|
||||
- Quote PostgreSQL identifiers and drop spaces from the creds file name [@MickLesk](https://github.com/MickLesk) ([core#89](https://github.com/community-scripts/core/pull/89))
|
||||
- Find the default Rust toolchain in current rustup output [@MickLesk](https://github.com/MickLesk) ([core#88](https://github.com/community-scripts/core/pull/88))
|
||||
|
||||
## 2026-10-03
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Keeper.sh ([#17659](https://github.com/community-scripts/ProxmoxVE/pull/17659))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- hermesagent: download the installer before running it [@MickLesk](https://github.com/MickLesk) ([#17666](https://github.com/community-scripts/ProxmoxVE/pull/17666))
|
||||
- frigate: raise the Node heap for the web build [@MickLesk](https://github.com/MickLesk) ([#17667](https://github.com/community-scripts/ProxmoxVE/pull/17667))
|
||||
- Tracearr: workaround for failing map tiles download and data preservation [@durzo](https://github.com/durzo) ([#17661](https://github.com/community-scripts/ProxmoxVE/pull/17661))
|
||||
- fix(nginxproxymanager): clean yarn cache after builds to prevent disk growth [@PsycoStea](https://github.com/PsycoStea) ([#17653](https://github.com/community-scripts/ProxmoxVE/pull/17653))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Several Scripts: refactor msg_ blocks, generate passwords with helper, add CLEAN_INSTALL_KEEP [@MickLesk](https://github.com/MickLesk) ([#17655](https://github.com/community-scripts/ProxmoxVE/pull/17655))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- detect_repo_source: read the license header and fall back to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([core#85](https://github.com/community-scripts/core/pull/85))
|
||||
|
||||
## 2026-10-02
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- zerobyte: upgrade Bun on update [@MickLesk](https://github.com/MickLesk) ([#17646](https://github.com/community-scripts/ProxmoxVE/pull/17646))
|
||||
- sure: keep Active Storage uploads across updates [@MickLesk](https://github.com/MickLesk) ([#17648](https://github.com/community-scripts/ProxmoxVE/pull/17648))
|
||||
- twenty: run upstreams upgrade command instead of the removed setup-db.ts [@MickLesk](https://github.com/MickLesk) ([#17647](https://github.com/community-scripts/ProxmoxVE/pull/17647))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- NPM: Refactor / Fix npm openresty user and certbot version [@MickLesk](https://github.com/MickLesk) ([#17649](https://github.com/community-scripts/ProxmoxVE/pull/17649))
|
||||
- several scripts: use the release helpers instead of hand-rolled version checks and downloads [@MickLesk](https://github.com/MickLesk) ([#17625](https://github.com/community-scripts/ProxmoxVE/pull/17625))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- QoL: message blocks, clean-install keep list, password helper, network limits [@MickLesk](https://github.com/MickLesk) ([core#61](https://github.com/community-scripts/core/pull/61))
|
||||
- FileBrowser Quantum: migrate the config and database to v2 [@MickLesk](https://github.com/MickLesk) ([#17815](https://github.com/community-scripts/ProxmoxVE/pull/17815))
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
_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: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/chattocorp/chatto
|
||||
|
||||
APP="Chatto"
|
||||
var_tags="${var_tags:-chat;messaging}"
|
||||
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}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/chatto ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "chatto" "chattocorp/chatto"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop chatto
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "chatto" "chattocorp/chatto" "prebuild" "latest" "/opt/chatto" "chatto_Linux_$(arch_resolve x86_64 arm64).tar.gz"
|
||||
chmod +x /opt/chatto/chatto
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start chatto
|
||||
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}Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:4000${CL}"
|
||||
+1
-1
@@ -36,7 +36,7 @@ function update_script() {
|
||||
systemctl stop cinephage
|
||||
msg_ok "Stopped Cinephage"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
|
||||
|
||||
msg_info "Building Cinephage"
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
_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: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://coolify.io/
|
||||
|
||||
APP="Coolify"
|
||||
var_tags="${var_tags:-docker;paas}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-30}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
ADDON_SCRIPT="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh"
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /data/coolify ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_warn "⚠️ ${APP} has been migrated to an addon script."
|
||||
echo ""
|
||||
echo -e "${INFO}${YW}This is a one-time migration. After this, you can update ${APP} anytime with:${CL}"
|
||||
echo -e "${TAB}${TAB}${GN}update_coolify${CL} or ${GN}bash <(curl -fsSL ${ADDON_SCRIPT})${CL}"
|
||||
echo ""
|
||||
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
|
||||
if [[ ! "${CONFIRM,,}" =~ ^(y|yes)$ ]]; then
|
||||
msg_warn "Migration skipped. The old update will continue to work for now."
|
||||
msg_info "Updating ${APP} (legacy)"
|
||||
$STD bash <(curl -fsSL https://cdn.coollabs.io/coolify/install.sh)
|
||||
msg_ok "Updated ${APP}"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_coolify 2>/dev/null || true
|
||||
msg_ok "Migration complete"
|
||||
|
||||
echo -e "${INFO}${YW}Running addon update${CL}"
|
||||
type=update bash <(curl -fsSL "${ADDON_SCRIPT}")
|
||||
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}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
|
||||
+1
-1
@@ -26,7 +26,7 @@ function update_script() {
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
ensure_dependencies build-essential
|
||||
|
||||
if command -v cross-seed &>/dev/null; then
|
||||
|
||||
+10
-38
@@ -38,57 +38,29 @@ function update_script() {
|
||||
$STD systemctl stop databasus
|
||||
msg_ok "Stopped Databasus"
|
||||
|
||||
create_backup /opt/databasus/.env
|
||||
|
||||
msg_info "Ensuring Database Clients"
|
||||
# Create PostgreSQL version symlinks for compatibility
|
||||
for v in 12 13 14 15 16 18; do
|
||||
ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v
|
||||
done
|
||||
# Install MongoDB Database Tools via direct .deb (no APT repo for Debian 13)
|
||||
if ! command -v mongodump &>/dev/null; then
|
||||
[[ "$(get_os_info id)" == "ubuntu" ]] && MONGO_DIST="ubuntu2204" || MONGO_DIST="debian12"
|
||||
MONGO_ARCH=$(arch_resolve "x86_64" "arm64")
|
||||
# MongoDB only publishes arm64 builds for Ubuntu
|
||||
[[ "$MONGO_ARCH" == "arm64" ]] && MONGO_DIST="ubuntu2204"
|
||||
fetch_and_deploy_from_url "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-${MONGO_DIST}-${MONGO_ARCH}-100.16.1.deb"
|
||||
fi
|
||||
ensure_dependencies mariadb-client
|
||||
mkdir -p /usr/local/mariadb-{10.6,12.1}/bin /usr/local/mysql-{5.7,8.0,8.4,9}/bin /usr/local/mongodb-database-tools/bin
|
||||
[[ -f /usr/bin/mongodump ]] && ln -sf /usr/bin/mongodump /usr/local/mongodb-database-tools/bin/mongodump
|
||||
[[ -f /usr/bin/mongorestore ]] && ln -sf /usr/bin/mongorestore /usr/local/mongodb-database-tools/bin/mongorestore
|
||||
# Create MariaDB and MySQL client symlinks for compatibility
|
||||
for dir in /usr/local/mariadb-{10.6,12.1}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mariadb-dump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mariadb"
|
||||
done
|
||||
for dir in /usr/local/mysql-{5.7,8.0,8.4,9}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mysqldump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mysql"
|
||||
done
|
||||
msg_ok "Ensured Database Clients"
|
||||
|
||||
ensure_dependencies libedit2 libgssapi-krb5-2 libncurses6 libreadline8t64
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus"
|
||||
GO_VERSION="$(grep -m1 '^go ' /opt/databasus/backend/go.mod | awk '{print $2}')" setup_go
|
||||
|
||||
msg_info "Updating Databasus"
|
||||
RELEASE=$(cat ~/.databasus)
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
cd /opt/databasus/frontend
|
||||
|
||||
$STD corepack prepare pnpm@latest --activate
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm run build
|
||||
$STD env VITE_APP_VERSION="${RELEASE}" pnpm run build
|
||||
cd /opt/databasus/backend
|
||||
$STD go mod download
|
||||
$STD /root/go/bin/swag init -g cmd/main.go -o swagger
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$(arch_resolve) go build -o databasus ./cmd
|
||||
mv /opt/databasus/backend/databasus /opt/databasus/databasus
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH="$(arch_resolve)" go build -o /opt/databasus/databasus ./cmd/databasus
|
||||
mkdir -p /opt/databasus/agent-binaries
|
||||
for arch in amd64 arm64; do
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -ldflags "-X main.Version=${RELEASE}" -o /opt/databasus/agent-binaries/databasus-verification-agent-linux-$arch ./cmd/verification-agent
|
||||
done
|
||||
mkdir -p /opt/databasus/ui/build
|
||||
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
|
||||
cp -r /opt/databasus/backend/migrations /opt/databasus/
|
||||
sed -i '/^APP_VERSION=/d' /.env
|
||||
echo "APP_VERSION=${RELEASE}" >>/.env
|
||||
msg_ok "Updated Databasus"
|
||||
|
||||
restore_backup
|
||||
chown -R postgres:postgres /opt/databasus
|
||||
|
||||
if ! grep -q "EnvironmentFile=/.env" /etc/systemd/system/databasus.service; then
|
||||
|
||||
@@ -4,17 +4,17 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Kristian Skov
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-9.0&tabs=linux-ubuntu
|
||||
# Source: https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian
|
||||
|
||||
APP="Dotnet ASP Web API"
|
||||
var_tags="${var_tags:-web}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-ubuntu}"
|
||||
var_version="${var_version:-24.04}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-0}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
@@ -43,4 +43,4 @@ description
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following IP:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}${IP}:80${CL}"
|
||||
echo -e "${GATEWAY}${BGN}${IP}:80${CL}"
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
_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: Masked-Kunsiquat
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.espocrm.com/ | Github: https://github.com/espocrm/espocrm
|
||||
|
||||
APP="EspoCRM"
|
||||
var_tags="${var_tags:-crm;business}"
|
||||
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}"
|
||||
|
||||
export var_admin_user="${var_admin_user:-}"
|
||||
export var_admin_pass="${var_admin_pass:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/espocrm ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "espo-crm" "espocrm/espocrm"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop nginx cron php8.4-fpm
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data custom client/custom" fetch_and_deploy_gh_release "espo-crm" "espocrm/espocrm" "prebuild" "latest" "/opt/espocrm" "EspoCRM-*.zip"
|
||||
|
||||
msg_info "Migrating EspoCRM"
|
||||
cd /opt/espocrm
|
||||
$STD php bin/command clear-cache
|
||||
if ! $STD php bin/command migrate; then
|
||||
chown -R www-data:www-data /opt/espocrm
|
||||
systemctl start php8.4-fpm
|
||||
systemctl start cron nginx
|
||||
msg_error "Migration failed; customizations may be incompatible. Check /opt/espocrm/data/logs, then run update again."
|
||||
exit 1
|
||||
fi
|
||||
chown -R www-data:www-data /opt/espocrm
|
||||
msg_ok "Migrated EspoCRM"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start php8.4-fpm
|
||||
systemctl start cron nginx
|
||||
msg_ok "Started Services"
|
||||
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}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
_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: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mozilla-services/syncstorage-rs
|
||||
|
||||
APP="Firefox-Syncserver"
|
||||
var_tags="${var_tags:-sync;firefox}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-3072}"
|
||||
var_disk="${var_disk:-10}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
#var_arm64="${var_arm64:-yes}" # built from source, nothing x86-only, but not yet run on arm64
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /usr/local/bin/syncserver ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs"; then
|
||||
msg_info "Stopping Firefox Syncserver"
|
||||
systemctl stop firefox-syncserver
|
||||
msg_ok "Stopped Firefox Syncserver"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs" "tarball"
|
||||
setup_rust
|
||||
|
||||
msg_info "Building Firefox Syncserver (Patience)"
|
||||
cd /opt/firefox-syncserver
|
||||
$STD cargo build --release --locked --no-default-features --features=syncstorage-db/postgres --features=tokenserver-db/postgres --features=py_verifier
|
||||
cp target/release/syncserver /usr/local/bin/syncserver
|
||||
rm -rf target
|
||||
$STD uv venv --python /usr/bin/python3 /opt/firefox-syncserver/.venv
|
||||
$STD uv pip install --python /opt/firefox-syncserver/.venv/bin/python pyfxa tokenlib cryptography
|
||||
msg_ok "Built Firefox Syncserver"
|
||||
|
||||
msg_info "Starting Firefox Syncserver"
|
||||
systemctl start firefox-syncserver
|
||||
msg_ok "Started Firefox Syncserver"
|
||||
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}In Firefox, set identity.sync.tokenserver.uri in about:config to:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000/1.0/sync/1.5${CL}"
|
||||
+23
-6
@@ -45,15 +45,32 @@ function update_script() {
|
||||
export CGO_ENABLED=0
|
||||
$STD go mod download
|
||||
$STD go build -o /usr/local/bin/gotenberg \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v9/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
cmd/gotenberg/main.go
|
||||
msg_ok "Built gotenberg"
|
||||
|
||||
msg_info "Updating unoconverter"
|
||||
UNOCONVERTER_VERSION=$(get_latest_github_release "gotenberg/unoconverter" "false")
|
||||
download_file "https://raw.githubusercontent.com/gotenberg/unoconverter/${UNOCONVERTER_VERSION}/unoconv" /usr/local/bin/unoconverter
|
||||
chmod +x /usr/local/bin/unoconverter
|
||||
msg_ok "Updated unoconverter"
|
||||
LOK_VERSION="$(awk -F= '/^ARG LOK_VERSION=/{print $2}' /opt/gotenberg/build/Dockerfile)"
|
||||
fetch_and_deploy_gh_release "lok" "gotenberg/lok" "prebuild" "$LOK_VERSION" "/usr/local/bin" "lok_${LOK_VERSION#v}_linux_$(arch_resolve "amd64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Migrating to Gotenberg 9"
|
||||
CODENAME=$(get_os_info codename)
|
||||
setup_deb822_repo \
|
||||
"debian-backports" \
|
||||
"file:///usr/share/keyrings/debian-archive-keyring.gpg" \
|
||||
"http://deb.debian.org/debian" \
|
||||
"${CODENAME}-backports" \
|
||||
"main"
|
||||
$STD apt install -y fonts-urw-base35
|
||||
$STD apt install -y -t "${CODENAME}-backports" \
|
||||
libreoffice-writer libreoffice-calc libreoffice-impress libreoffice-draw libreoffice-math
|
||||
$STD apt purge -y python3-uno python-is-python3 pdftk-java qpdf libimage-exiftool-perl
|
||||
$STD apt autoremove -y
|
||||
rm -rf /usr/local/bin/unoconverter /usr/local/bin/pdfcpu /opt/pdfcpu
|
||||
sed -i '/^Environment=\(UNOCONVERTER\|PDFTK\|QPDF\|EXIFTOOL\|PDFCPU\)_BIN_PATH=/d' /etc/systemd/system/gotenberg.service
|
||||
grep -q '^Environment=LOK_BIN_PATH=' /etc/systemd/system/gotenberg.service ||
|
||||
sed -i '/^Environment=LIBREOFFICE_BIN_PATH=/a Environment=LOK_BIN_PATH=/usr/local/bin/lok' /etc/systemd/system/gotenberg.service
|
||||
systemctl daemon-reload
|
||||
msg_ok "Migrated to Gotenberg 9"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start gotenberg
|
||||
|
||||
+2
-27
@@ -44,7 +44,7 @@ function update_script() {
|
||||
docker pull "${CONTAINER_IMAGE}"
|
||||
LATEST_IMAGE="$(docker inspect --format "{{.Id}}" --type image "${CONTAINER_IMAGE}")"
|
||||
if [[ "${RUNNING_IMAGE}" != "${LATEST_IMAGE}" ]]; then
|
||||
pip install -U runlike
|
||||
pip install -U runlike --break-system-packages
|
||||
echo "Updating ${container} image ${CONTAINER_IMAGE}"
|
||||
DOCKER_COMMAND="$(runlike --use-volume-id "${container}")"
|
||||
docker rm --force "${container}"
|
||||
@@ -70,32 +70,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
if [ "$UPD" == "4" ]; then
|
||||
msg_info "Installing FileBrowser"
|
||||
RELEASE=$(curl -fsSL https://api.github.com/repos/filebrowser/filebrowser/releases/latest | grep -o '"tag_name": ".*"' | sed 's/"//g' | sed 's/tag_name: //g')
|
||||
$STD curl -fsSL https://github.com/filebrowser/filebrowser/releases/download/v2.23.0/linux-$(arch_resolve)-filebrowser.tar.gz | tar -xzv -C /usr/local/bin
|
||||
$STD filebrowser config init -a '0.0.0.0'
|
||||
$STD filebrowser config set -a '0.0.0.0'
|
||||
$STD filebrowser users add admin community-scripts.org --perm.admin
|
||||
msg_ok "Installed FileBrowser"
|
||||
|
||||
msg_info "Creating Service"
|
||||
service_path="/etc/systemd/system/filebrowser.service"
|
||||
echo "[Unit]
|
||||
Description=Filebrowser
|
||||
After=network-online.target
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/root/
|
||||
ExecStart=/usr/local/bin/filebrowser -r /
|
||||
[Install]
|
||||
WantedBy=default.target" >$service_path
|
||||
|
||||
$STD systemctl enable --now filebrowser
|
||||
msg_ok "Created Service"
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "FileBrowser should be reachable by going to the following URL.
|
||||
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
+40
-16
@@ -75,7 +75,7 @@ EOF
|
||||
BASE_DIR=${STAGING_DIR}/base-images
|
||||
SOURCE_DIR=${STAGING_DIR}/image-source
|
||||
cd /tmp
|
||||
RELEASE="v3.3.6"
|
||||
RELEASE="v3.4.1"
|
||||
if [[ -f ~/.intel_version ]]; then
|
||||
INTEL_SRC="https://raw.githubusercontent.com/immich-app/immich/${RELEASE}/machine-learning"
|
||||
curl -fsSL "${INTEL_SRC}/scripts/install-intel-runtime.sh" -o ./intel-runtime 2>/dev/null ||
|
||||
@@ -109,7 +109,7 @@ EOF
|
||||
libraries=("libjxl" "jpegli" "libheif" "libraw" "imagemagick" "libvips")
|
||||
cd "$BASE_DIR"
|
||||
msg_warn "Checking for updates to custom image-processing libraries (recompile time: 2-15min per library)"
|
||||
ensure_dependencies liblcms2-dev libjpeg62-turbo-dev libspng-dev libexif-dev libopenjp2-7-dev
|
||||
ensure_dependencies liblcms2-dev libjpeg62-turbo-dev libspng-dev libexif-dev libopenjp2-7-dev pigz
|
||||
$STD git pull
|
||||
for library in "${libraries[@]}"; do
|
||||
compile_"$library"
|
||||
@@ -131,7 +131,18 @@ EOF
|
||||
|
||||
if check_for_gh_release "Immich" "immich-app/immich" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
|
||||
msg_warn "The update takes 5-15 minutes, do not interrupt it"
|
||||
PREV_IMMICH="$(cat ~/.immich)"
|
||||
PREV_IMMICH="${CS_PREVIOUS_VERSION:-$(cat ~/.immich)}"
|
||||
INSTALL_DIR="/opt/${APP}"
|
||||
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
|
||||
SRC_DIR="${INSTALL_DIR}/source"
|
||||
APP_DIR="${INSTALL_DIR}/app"
|
||||
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
|
||||
ML_DIR="${APP_DIR}/machine-learning"
|
||||
GEO_DIR="${INSTALL_DIR}/geodata"
|
||||
|
||||
setup_uv
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
|
||||
|
||||
# An interrupted update leaves app/ empty, so there may be no immich-admin to call.
|
||||
if [[ "$PREV_IMMICH" > "2.5.1" && -x /opt/immich/app/bin/immich-admin ]]; then
|
||||
msg_info "Enabling Maintenance Mode"
|
||||
@@ -141,6 +152,7 @@ EOF
|
||||
$STD cd -
|
||||
msg_ok "Enabled Maintenance Mode"
|
||||
fi
|
||||
[[ -x /opt/immich/app/bin/immich-admin ]] || export MAINT_MODE=1
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop immich-web
|
||||
systemctl stop immich-ml
|
||||
@@ -164,14 +176,6 @@ EOF
|
||||
fi
|
||||
ensure_dependencies ccache gcc-13 g++-13
|
||||
|
||||
INSTALL_DIR="/opt/${APP}"
|
||||
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
|
||||
SRC_DIR="${INSTALL_DIR}/source"
|
||||
APP_DIR="${INSTALL_DIR}/app"
|
||||
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
|
||||
ML_DIR="${APP_DIR}/machine-learning"
|
||||
GEO_DIR="${INSTALL_DIR}/geodata"
|
||||
|
||||
[[ -f "$ML_DIR"/ml_start.sh ]] && cp "$ML_DIR"/ml_start.sh "$INSTALL_DIR"
|
||||
if grep -qs "set -a" "$APP_DIR"/bin/start.sh && grep -qs "warnings" "$APP_DIR"/bin/start.sh; then
|
||||
cp "$APP_DIR"/bin/start.sh "$INSTALL_DIR"
|
||||
@@ -193,8 +197,6 @@ EOF
|
||||
rm -rf "${APP_DIR:?}"/*
|
||||
)
|
||||
|
||||
setup_uv
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
|
||||
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
export CI=1
|
||||
@@ -232,7 +234,7 @@ EOF
|
||||
# plugins
|
||||
cd "$SRC_DIR"
|
||||
export MISE_TRUSTED_CONFIG_PATHS="$SRC_DIR"/mise.toml
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,npm:@openapitools/openapi-generator-cli
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,restic,npm:@openapitools/openapi-generator-cli
|
||||
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
|
||||
mise_ok=0
|
||||
for i in 1 2 3; do
|
||||
@@ -249,7 +251,8 @@ EOF
|
||||
# extism-js ships as a bare gzip-compressed single binary (.gz) that
|
||||
# fetch_and_deploy_gh_release cannot deploy; fetch + gunzip it directly.
|
||||
EXTISM_ARCH="$(arch_resolve x86_64 aarch64)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-${EXTISM_ARCH}-linux-v1.6.0.gz"
|
||||
EXTISM_VERSION="$(grep -oP '"github:extism/js-pdk"\s*=\s*"\K[^"]+' "$SRC_DIR"/mise.toml)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/${EXTISM_VERSION}/extism-js-${EXTISM_ARCH}-linux-${EXTISM_VERSION}.gz"
|
||||
gunzip -f /tmp/extism-js.gz
|
||||
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
|
||||
rm -f /tmp/extism-js
|
||||
@@ -274,6 +277,27 @@ EOF
|
||||
cp ./packages/plugin-core/manifest.json "$PLUGIN_DIR"
|
||||
msg_ok "Updated Immich server, web, cli and plugins"
|
||||
|
||||
RESTIC_REF="$(grep -oP 'futo-org/restic:sha-\K[0-9a-f]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
RESTIC_PROXY_REF="$(grep -oP 'futo-org/backups/restic-proxy:\K[^@]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
if [[ "$(cat ~/.immich_restic 2>/dev/null)" != "${RESTIC_REF} ${RESTIC_PROXY_REF}" || ! -x /usr/local/bin/restic ]]; then
|
||||
FUTO_DIR=/opt/staging/futo
|
||||
rm -rf "$FUTO_DIR"
|
||||
$STD git clone --filter=blob:none --no-checkout https://github.com/futo-org/restic "$FUTO_DIR"/restic
|
||||
$STD git -C "$FUTO_DIR"/restic checkout "$RESTIC_REF"
|
||||
$STD git clone --depth 1 --filter=blob:none --sparse -b "$RESTIC_PROXY_REF" https://github.com/futo-org/backups "$FUTO_DIR"/backups
|
||||
$STD git -C "$FUTO_DIR"/backups sparse-checkout set packages/restic-proxy
|
||||
GO_VERSION="$(awk '/^go /{print $2}' "$FUTO_DIR"/backups/packages/restic-proxy/go.mod)" setup_go
|
||||
msg_info "Building restic for FUTO Backups"
|
||||
cd "$FUTO_DIR"/restic
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic ./cmd/restic
|
||||
cd "$FUTO_DIR"/backups/packages/restic-proxy
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic-proxy .
|
||||
cd "$SRC_DIR"
|
||||
rm -rf "$FUTO_DIR"
|
||||
echo "${RESTIC_REF} ${RESTIC_PROXY_REF}" >~/.immich_restic
|
||||
msg_ok "Built restic for FUTO Backups"
|
||||
fi
|
||||
|
||||
cd "$SRC_DIR"/machine-learning
|
||||
mkdir -p "$ML_DIR"
|
||||
# chown excluding upload dir contents (may be a mount with restricted permissions)
|
||||
@@ -419,7 +443,7 @@ function compile_libjxl() {
|
||||
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
|
||||
cd "$SOURCE"
|
||||
$STD git reset --hard "$LIBJXL_REVISION"
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow -- third_party
|
||||
mkdir build
|
||||
cd build
|
||||
$STD cmake \
|
||||
|
||||
@@ -32,14 +32,14 @@ function update_script() {
|
||||
fi
|
||||
|
||||
if check_for_gh_tag "kan" "kanbn/kan"; then
|
||||
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop kan
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/kan/.env
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_tag "kan" "kanbn/kan" "latest"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Building Application"
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
APP="MyIP"
|
||||
var_tags="${var_tags:-network}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
@@ -30,7 +30,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
|
||||
if check_for_gh_release "myip" "jason5ng32/MyIP"; then
|
||||
msg_info "Stopping Services"
|
||||
|
||||
+6
-1
@@ -32,9 +32,14 @@ function update_script() {
|
||||
|
||||
if check_for_gh_release "openobserve" "openobserve/openobserve"; then
|
||||
msg_info "Updating OpenObserve"
|
||||
if grep -q "^ExecStop=killall" /etc/systemd/system/openobserve.service; then
|
||||
sed -i "/^ExecStop=killall/d" /etc/systemd/system/openobserve.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
systemctl stop openobserve
|
||||
RELEASE=$(get_latest_github_release "openobserve/openobserve")
|
||||
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
|
||||
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve --no-same-owner
|
||||
echo "${RELEASE}" >~/.openobserve
|
||||
systemctl start openobserve
|
||||
msg_ok "Updated OpenObserve"
|
||||
msg_ok "Updated successfully!"
|
||||
|
||||
+41
-25
@@ -32,36 +32,52 @@ function update_script() {
|
||||
|
||||
if check_for_gh_release "paperless-ai" "clusterzx/paperless-ai"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop paperless-ai paperless-rag
|
||||
systemctl stop paperless-ai
|
||||
if [[ -f /etc/systemd/system/paperless-rag.service ]]; then
|
||||
systemctl stop paperless-rag
|
||||
fi
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up data"
|
||||
cp -r /opt/paperless-ai/data /opt/paperless-ai-data-backup
|
||||
msg_ok "Backed up data"
|
||||
|
||||
fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
|
||||
|
||||
msg_info "Restoring data"
|
||||
cp -r /opt/paperless-ai-data-backup/* /opt/paperless-ai/data/
|
||||
rm -rf /opt/paperless-ai-data-backup
|
||||
msg_ok "Restored data"
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data" fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
|
||||
|
||||
msg_info "Updating Paperless-AI"
|
||||
cd /opt/paperless-ai
|
||||
if [[ ! -d /opt/paperless-ai/venv ]]; then
|
||||
msg_info "Recreating Python venv"
|
||||
$STD python3 -m venv /opt/paperless-ai/venv
|
||||
fi
|
||||
source /opt/paperless-ai/venv/bin/activate
|
||||
$STD pip install --upgrade pip
|
||||
$STD pip install --no-cache-dir -r requirements.txt
|
||||
mkdir -p data/chromadb
|
||||
$STD npm ci --only=production
|
||||
export ONNXRUNTIME_NODE_INSTALL=skip PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
$STD npm ci --ignore-scripts --no-audit --no-fund
|
||||
$STD npm run build
|
||||
$STD npm prune --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
msg_ok "Updated Paperless-AI"
|
||||
|
||||
if ! grep -q "dist/server/index.js" /etc/systemd/system/paperless-ai.service; then
|
||||
msg_info "Migrating services to Paperless-AI 4"
|
||||
if [[ -f /etc/systemd/system/paperless-rag.service ]]; then
|
||||
systemctl disable -q paperless-rag
|
||||
rm -f /etc/systemd/system/paperless-rag.service
|
||||
fi
|
||||
cat <<EOF >/etc/systemd/system/paperless-ai.service
|
||||
[Unit]
|
||||
Description=PaperlessAI Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/paperless-ai
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="PAPERLESS_AI_PORT=3000"
|
||||
Environment="PAPERLESS_AI_DATA_DIR=/opt/paperless-ai/data"
|
||||
Environment="HF_HUB_DISABLE_TELEMETRY=1"
|
||||
ExecStart=/usr/bin/node dist/server/index.js
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
msg_ok "Migrated services to Paperless-AI 4"
|
||||
msg_warn "Settings and history from 3.x are migrated on first start. If this instance was reachable from the internet, read 'Upgrading from 3.x' in the upstream README first."
|
||||
fi
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start paperless-rag
|
||||
sleep 3
|
||||
systemctl start paperless-ai
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
@@ -73,7 +89,7 @@ start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
_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: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://pixelfed.org/
|
||||
|
||||
APP="Pixelfed"
|
||||
var_tags="${var_tags:-fediverse;social}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-10}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
# var_arm64="${var_arm64:-no}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/pixelfed ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "pixelfed" "pixelfed/pixelfed"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop pixelfed-horizon pixelfed-scheduler.timer
|
||||
msg_ok "Services stopped"
|
||||
|
||||
create_backup /opt/pixelfed/.env \
|
||||
/opt/pixelfed/storage
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "pixelfed" "pixelfed/pixelfed" "tarball" "latest" "/opt/pixelfed"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Updating Pixelfed"
|
||||
cd /opt/pixelfed
|
||||
chmod -R 755 /opt/pixelfed
|
||||
chmod -R 775 /opt/pixelfed/storage /opt/pixelfed/bootstrap/cache
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
$STD composer install --no-dev --no-ansi --no-interaction --optimize-autoloader
|
||||
chown -R pixelfed:pixelfed /opt/pixelfed
|
||||
$STD sudo -u pixelfed php artisan storage:link
|
||||
$STD sudo -u pixelfed php artisan migrate --force
|
||||
$STD sudo -u pixelfed php artisan route:cache
|
||||
$STD sudo -u pixelfed php artisan view:cache
|
||||
$STD sudo -u pixelfed php artisan config:cache
|
||||
$STD sudo -u pixelfed php artisan horizon:publish
|
||||
msg_ok "Updated Pixelfed"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start pixelfed-horizon pixelfed-scheduler.timer
|
||||
msg_ok "Services started"
|
||||
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}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
|
||||
echo -e "${INFO}${YW} Create an admin account with:${CL}"
|
||||
echo -e "${TAB}cd /opt/pixelfed && sudo -u pixelfed php artisan user:create"
|
||||
+7
-6
@@ -4,7 +4,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
|
||||
# Source: https://pocketbase.io/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
|
||||
|
||||
APP="Pocketbase"
|
||||
var_tags="${var_tags:-database}"
|
||||
@@ -29,20 +29,21 @@ function update_script() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if check_for_gh_release "pocketbase" "pocketbase/pocketbase"; then
|
||||
RELEASE=$(curl_with_retry "https://ganigeorgiev.com/api/collections/releases/records?filter=channel.repository.name%3D%22pocketbase%22%20%26%26%20channel.name%3D%22default%22&sort=-semver&skipTotal=1&perPage=1" "-" | jq -r '.items[0].tag' | sed 's/^v//')
|
||||
if [[ -n "$RELEASE" && "$RELEASE" != "$(cat ~/.pocketbase 2>/dev/null)" ]]; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop pocketbase
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Updating ${APP}"
|
||||
/opt/pocketbase/pocketbase update
|
||||
echo "${CHECK_UPDATE_RELEASE}" >~/.pocketbase
|
||||
msg_ok "Updated ${APP}"
|
||||
fetch_and_deploy_from_url "https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v${RELEASE}/pocketbase_${RELEASE}_linux_$(arch_resolve).zip" "/opt/pocketbase"
|
||||
echo "${RELEASE}" >~/.pocketbase
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start pocketbase
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at v${RELEASE}"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -66,33 +66,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
if [ "$UPD" == "3" ]; then
|
||||
msg_info "Installing FileBrowser"
|
||||
$STD curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||
$STD filebrowser config init -a '0.0.0.0'
|
||||
$STD filebrowser config set -a '0.0.0.0'
|
||||
$STD filebrowser users add admin community-scripts.org --perm.admin
|
||||
msg_ok "Installed FileBrowser"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/filebrowser.service
|
||||
[Unit]
|
||||
Description=Filebrowser
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/root/
|
||||
ExecStart=/usr/local/bin/filebrowser -r /
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
systemctl enable -q --now filebrowser
|
||||
msg_ok "Created Service"
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "FileBrowser should be reachable by going to the following URL.
|
||||
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
|
||||
exit
|
||||
fi
|
||||
if [ "$UPD" == "4" ]; then
|
||||
|
||||
+21
-9
@@ -107,8 +107,9 @@ function update_script() {
|
||||
ln -sfn "$ROMM_BASE"/resources /opt/romm/frontend/dist/assets/romm/resources
|
||||
ln -sfn "$ROMM_BASE"/assets /opt/romm/frontend/dist/assets/romm/assets
|
||||
if [[ -f /etc/angie/http.d/romm.conf ]]; then
|
||||
if ! grep -q "js_content decode.decodeBase64" /etc/angie/http.d/romm.conf; then
|
||||
msg_info "Adding missing /decode and /cache locations to Angie config"
|
||||
if ! grep -q "js_content decode.decodeBase64" /etc/angie/http.d/romm.conf ||
|
||||
! grep -q "coep_header" /etc/angie/http.d/romm.conf; then
|
||||
msg_info "Updating Angie config (decode/cache locations, player isolation headers)"
|
||||
dpkg -l angie-module-njs &>/dev/null || $STD apt install -y angie-module-njs
|
||||
grep -q "ngx_http_js_module.so" /etc/angie/angie.conf || sed -i '1i load_module modules/ngx_http_js_module.so;' /etc/angie/angie.conf
|
||||
mkdir -p /etc/angie/js "${ROMM_BASE}/cache"
|
||||
@@ -147,6 +148,20 @@ map \$http_upgrade \$connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# COEP/COOP for the EmulatorJS and js-dos players (SharedArrayBuffer needs a
|
||||
# crossOriginIsolated document). Matched on \$request_uri because try_files
|
||||
# rewrites \$uri to /index.html before add_header runs, so a per-location
|
||||
# add_header on the player route never fires. Mirrors upstream RomM's
|
||||
# docker/nginx/templates/default.conf.template.
|
||||
map \$request_uri \$coep_header {
|
||||
default "";
|
||||
~^/rom/.*/(ejs|jsdos)(\?|\$) "require-corp";
|
||||
}
|
||||
map \$request_uri \$coop_header {
|
||||
default "";
|
||||
~^/rom/.*/(ejs|jsdos)(\?|\$) "same-origin";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
@@ -155,6 +170,9 @@ server {
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
add_header Cache-Control "no-cache";
|
||||
add_header Cross-Origin-Embedder-Policy \$coep_header;
|
||||
add_header Cross-Origin-Opener-Policy \$coop_header;
|
||||
}
|
||||
|
||||
location /assets {
|
||||
@@ -164,12 +182,6 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location ~ ^/rom/.*/ejs\$ {
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||||
add_header Cross-Origin-Opener-Policy "same-origin";
|
||||
try_files \$uri /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://romm_backend;
|
||||
proxy_buffering off;
|
||||
@@ -209,7 +221,7 @@ server {
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Added /decode and /cache locations"
|
||||
msg_ok "Updated Angie config"
|
||||
else
|
||||
sed -i -e "s|alias .*/library/;|alias ${ROMM_BASE}/library/;|" \
|
||||
-e "s|alias .*/cache/;|alias ${ROMM_BASE}/cache/;|" /etc/angie/http.d/romm.conf
|
||||
|
||||
+3
-6
@@ -37,8 +37,6 @@ function update_script() {
|
||||
create_backup /root/.local/share/tunarr
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tunarr" "chrisbenincasa/tunarr" "prebuild" "latest" "/opt/tunarr" "*linux-$(arch_resolve "x64" "arm64").tar.gz"
|
||||
cd /opt/tunarr
|
||||
mv tunarr* tunarr
|
||||
restore_backup
|
||||
|
||||
msg_info "Starting Service"
|
||||
@@ -52,13 +50,12 @@ function update_script() {
|
||||
systemctl stop tunarr
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-7.1.tar.xz"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-*.tar.xz"
|
||||
|
||||
msg_info "Set ErsatzTV-ffmpeg links"
|
||||
chmod +x /opt/ErsatzTV-ffmpeg/bin/*
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffplay /usr/local/bin/ffplay
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffprobe /usr/local/bin/ffprobe
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffmpeg /usr/bin/ffmpeg
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffprobe /usr/bin/ffprobe
|
||||
msg_ok "ffmpeg links set"
|
||||
|
||||
msg_info "Starting Service"
|
||||
|
||||
@@ -56,3 +56,4 @@ msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:6969${CL}"
|
||||
echo -e "${INFO}${YW} Login: whisparr, password in ~/whisparr-eros.creds inside the container${CL}"
|
||||
+1
-1
@@ -45,7 +45,7 @@ function update_script() {
|
||||
msg_info "Running Post-Update Tasks"
|
||||
cd /opt/writefreely
|
||||
$STD ./writefreely db migrate
|
||||
ln -s /opt/writefreely/writefreely /usr/local/bin/writefreely
|
||||
ln -sf /opt/writefreely/writefreely /usr/local/bin/writefreely
|
||||
msg_ok "Ran Post-Update Tasks"
|
||||
|
||||
msg_info "Starting Services"
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ function update_script() {
|
||||
|
||||
create_backup /opt/yuvomi/data /opt/yuvomi/.env
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "tarball"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "prebuild" "latest" "/opt/yuvomi" "yuvomi-*-dist.tar.gz"
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
msg_info "Installing Node.js Dependencies"
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
APP="Zipline"
|
||||
var_tags="${var_tags:-file;sharing}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_ram="${var_ram:-3072}"
|
||||
var_disk="${var_disk:-5}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
|
||||
@@ -20,6 +20,9 @@ $STD apt install -y \
|
||||
git \
|
||||
memcached \
|
||||
libmemcached-tools
|
||||
# Debian's memcached also listens on ::1 and does not start when IPv6 is disabled
|
||||
sed -i '/^-l ::1$/d' /etc/memcached.conf
|
||||
systemctl restart memcached
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
@@ -48,6 +48,7 @@ chown -R archivebox:archivebox /opt/archivebox/{data,.npm,.cache,.local}
|
||||
chmod -R 755 /opt/archivebox/data
|
||||
$STD uv pip install archivebox --system --break-system-packages
|
||||
cd /opt/archivebox/data
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
expect <<EOF
|
||||
set timeout -1
|
||||
log_user 0
|
||||
@@ -62,13 +63,18 @@ expect "Email address"
|
||||
send "\r"
|
||||
|
||||
expect "Password"
|
||||
send "community-scripts.org\r"
|
||||
send "${ADMIN_PASS}\r"
|
||||
|
||||
expect "Password (again)"
|
||||
send "community-scripts.org\r"
|
||||
send "${ADMIN_PASS}\r"
|
||||
|
||||
expect eof
|
||||
EOF
|
||||
{
|
||||
echo "ArchiveBox Credentials"
|
||||
echo "Username: archivebox"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/archivebox.creds
|
||||
msg_ok "Installed ArchiveBox"
|
||||
|
||||
msg_info "Creating Service"
|
||||
|
||||
@@ -63,12 +63,19 @@ else
|
||||
fi
|
||||
echo "$AUTHELIA_URL" >/etc/authelia/.authelia_url
|
||||
|
||||
AUTHELIA_PASS=$(random_password 20)
|
||||
AUTHELIA_HASH=$(authelia crypto hash generate argon2 --password "$AUTHELIA_PASS" | awk '/Digest:/{print $2}')
|
||||
{
|
||||
echo "Authelia Credentials"
|
||||
echo "Username: authelia"
|
||||
echo "Password: ${AUTHELIA_PASS}"
|
||||
} >~/authelia.creds
|
||||
cat <<EOF >/etc/authelia/users.yml
|
||||
users:
|
||||
authelia:
|
||||
disabled: false
|
||||
displayname: "Authelia Admin"
|
||||
password: "\$argon2id\$v=19\$m=65536,t=3,p=4\$ZBopMzXrzhHXPEZxRDVT2w\$SxWm96DwhOsZyn34DLocwQEIb4kCDsk632PuiMdZnig"
|
||||
password: "${AUTHELIA_HASH}"
|
||||
groups: []
|
||||
EOF
|
||||
cat <<EOF >/etc/authelia/configuration.yml
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/chattocorp/chatto
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
fetch_and_deploy_gh_release "chatto" "chattocorp/chatto" "prebuild" "latest" "/opt/chatto" "chatto_Linux_$(arch_resolve x86_64 arm64).tar.gz"
|
||||
chmod +x /opt/chatto/chatto
|
||||
|
||||
msg_info "Configuring Chatto"
|
||||
mkdir -p /opt/chatto_data
|
||||
cd /opt/chatto
|
||||
$STD ./chatto init
|
||||
mv /opt/chatto/chatto.toml /opt/chatto_data/chatto.toml
|
||||
sed -i \
|
||||
-e "/^\[webserver\]/,/^\[/ s|^url = .*|url = 'http://${LOCAL_IP}:4000'|" \
|
||||
-e "/^\[nats.embedded\]/,/^\[/ s|^data_dir = .*|data_dir = '/opt/chatto_data/data'|" \
|
||||
-e '/^# \[operator_api\]$/,/^$/ { s|^# \[operator_api\]$|[operator_api]|; s|^# enabled = false$|enabled = true| }' \
|
||||
/opt/chatto_data/chatto.toml
|
||||
msg_ok "Configured Chatto"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/chatto.service
|
||||
[Unit]
|
||||
Description=Chatto
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/chatto
|
||||
ExecStart=/opt/chatto/chatto run -c /opt/chatto_data/chatto.toml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now chatto
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -24,7 +24,7 @@ $STD apt install -y \
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
setup_ffmpeg
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
|
||||
|
||||
msg_info "Building Cinephage"
|
||||
|
||||
@@ -17,7 +17,7 @@ msg_info "Installing Dependencies"
|
||||
$STD apt install -y build-essential
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
|
||||
msg_info "Setup Cross-Seed"
|
||||
$STD npm install cross-seed@latest -g
|
||||
|
||||
@@ -16,119 +16,47 @@ update_os
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
nginx \
|
||||
valkey \
|
||||
mariadb-client \
|
||||
rclone
|
||||
libedit2 \
|
||||
libgssapi-krb5-2 \
|
||||
libncurses6 \
|
||||
libreadline8t64
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_VERSION="17" setup_postgresql
|
||||
NODE_VERSION="24" NODE_MODULE="corepack" setup_nodejs
|
||||
|
||||
msg_info "Installing Database Clients"
|
||||
# Create PostgreSQL version symlinks for compatibility
|
||||
for v in 12 13 14 15 16 18; do
|
||||
ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v
|
||||
done
|
||||
# Install MongoDB Database Tools via direct .deb (no APT repo for Debian 13)
|
||||
[[ "$(get_os_info id)" == "ubuntu" ]] && MONGO_DIST="ubuntu2204" || MONGO_DIST="debian12"
|
||||
# MongoDB only publishes arm64 builds for Ubuntu
|
||||
[[ "$(arch_resolve "x86_64" "arm64")" == "arm64" ]] && MONGO_DIST="ubuntu2204"
|
||||
MONGO_VERSION=$(get_latest_gh_tag "mongodb/mongo-tools" "100." || echo "100.16.1")
|
||||
fetch_and_deploy_from_url "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-${MONGO_DIST}-$(arch_resolve "x86_64" "arm64")-${MONGO_VERSION}.deb" ""
|
||||
mkdir -p /usr/local/mongodb-database-tools/bin
|
||||
[[ -f /usr/bin/mongodump ]] && ln -sf /usr/bin/mongodump /usr/local/mongodb-database-tools/bin/mongodump
|
||||
[[ -f /usr/bin/mongorestore ]] && ln -sf /usr/bin/mongorestore /usr/local/mongodb-database-tools/bin/mongorestore
|
||||
# Create MariaDB and MySQL client symlinks for compatibility
|
||||
mkdir -p /usr/local/mariadb-{10.6,12.1}/bin /usr/local/mysql-{5.7,8.0,8.4,9}/bin
|
||||
for dir in /usr/local/mariadb-{10.6,12.1}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mariadb-dump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mariadb"
|
||||
done
|
||||
for dir in /usr/local/mysql-{5.7,8.0,8.4,9}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mysqldump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mysql"
|
||||
done
|
||||
msg_ok "Installed Database Clients"
|
||||
|
||||
fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus"
|
||||
GO_VERSION="$(grep -m1 '^go ' /opt/databasus/backend/go.mod | awk '{print $2}')" setup_go
|
||||
|
||||
msg_info "Building Databasus (Patience)"
|
||||
RELEASE=$(cat ~/.databasus)
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
cd /opt/databasus/frontend
|
||||
|
||||
$STD corepack prepare pnpm@latest --activate
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm run build
|
||||
$STD env VITE_APP_VERSION="${RELEASE}" pnpm run build
|
||||
cd /opt/databasus/backend
|
||||
$STD go mod tidy
|
||||
$STD go mod download
|
||||
$STD go install github.com/swaggo/swag/cmd/swag@latest
|
||||
$STD /root/go/bin/swag init -g cmd/main.go -o swagger
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$(arch_resolve) go build -o databasus ./cmd
|
||||
mv /opt/databasus/backend/databasus /opt/databasus/databasus
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH="$(arch_resolve)" go build -o /opt/databasus/databasus ./cmd/databasus
|
||||
mkdir -p /opt/databasus/agent-binaries
|
||||
for arch in amd64 arm64; do
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -ldflags "-X main.Version=${RELEASE}" -o /opt/databasus/agent-binaries/databasus-verification-agent-linux-$arch ./cmd/verification-agent
|
||||
done
|
||||
mkdir -p /databasus-data/{pgdata,temp,backups,data,logs}
|
||||
mkdir -p /opt/databasus/ui/build
|
||||
mkdir -p /opt/databasus/migrations
|
||||
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
|
||||
cp -r /opt/databasus/backend/migrations/* /opt/databasus/migrations/
|
||||
chown -R postgres:postgres /databasus-data
|
||||
msg_ok "Built Databasus"
|
||||
|
||||
msg_info "Configuring Databasus"
|
||||
JWT_SECRET=$(openssl rand -hex 32)
|
||||
ENCRYPTION_KEY=$(openssl rand -hex 32)
|
||||
# Install goose for migrations
|
||||
$STD go install github.com/pressly/goose/v3/cmd/goose@latest
|
||||
ln -sf /root/go/bin/goose /usr/local/bin/goose
|
||||
cat <<EOF >/.env
|
||||
# Environment
|
||||
ENV_MODE=production
|
||||
|
||||
# Server
|
||||
SERVER_PORT=4005
|
||||
SERVER_HOST=0.0.0.0
|
||||
|
||||
# Database
|
||||
APP_VERSION=${RELEASE}
|
||||
DATABASE_DSN=host=localhost user=postgres password=postgres dbname=databasus port=5432 sslmode=disable
|
||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable
|
||||
|
||||
# Migrations
|
||||
GOOSE_DRIVER=postgres
|
||||
GOOSE_DBSTRING=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable
|
||||
GOOSE_MIGRATION_DIR=/opt/databasus/migrations
|
||||
|
||||
# Valkey (Redis-compatible cache)
|
||||
VALKEY_HOST=localhost
|
||||
VALKEY_PORT=6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET=${JWT_SECRET}
|
||||
ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
|
||||
# Paths
|
||||
DATA_DIR=/databasus-data/data
|
||||
BACKUP_DIR=/databasus-data/backups
|
||||
LOG_DIR=/databasus-data/logs
|
||||
EOF
|
||||
chmod 600 /.env
|
||||
msg_ok "Configured Databasus"
|
||||
|
||||
msg_info "Configuring Valkey"
|
||||
cat <<EOF >/etc/valkey/valkey.conf
|
||||
port 6379
|
||||
bind 127.0.0.1
|
||||
protected-mode yes
|
||||
save ""
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
EOF
|
||||
systemctl enable -q --now valkey-server
|
||||
systemctl restart valkey-server
|
||||
msg_ok "Configured Valkey"
|
||||
|
||||
msg_info "Creating Database"
|
||||
# Configure PostgreSQL to allow local password auth for databasus
|
||||
PG_HBA="/etc/postgresql/17/main/pg_hba.conf"
|
||||
if ! grep -q "databasus" "$PG_HBA"; then
|
||||
sed -i '/^local\s*all\s*all/i local databasus postgres trust' "$PG_HBA"
|
||||
@@ -143,8 +71,8 @@ msg_info "Creating Databasus Service"
|
||||
cat <<EOF >/etc/systemd/system/databasus.service
|
||||
[Unit]
|
||||
Description=Databasus - Database Backup Management
|
||||
After=network.target postgresql.service valkey.service
|
||||
Requires=postgresql.service valkey.service
|
||||
After=network.target postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Kristian Skov
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-9.0&tabs=linux-ubuntu
|
||||
# Source: https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
@@ -14,21 +14,36 @@ network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
apt_update_safe
|
||||
$STD apt install -y \
|
||||
ssh \
|
||||
software-properties-common
|
||||
|
||||
$STD add-apt-repository -y ppa:dotnet/backports
|
||||
$STD apt install -y \
|
||||
dotnet-sdk-9.0 \
|
||||
vsftpd \
|
||||
nginx
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
var_project_name="${var_project_name:-}"
|
||||
[[ -n "$var_project_name" ]] || read -r -p "${TAB3}Type the assembly name of the project: " var_project_name || true
|
||||
[[ -n "$var_project_name" ]] || { msg_error "No assembly name given; set var_project_name or answer the prompt"; exit 64; }
|
||||
setup_deb822_repo \
|
||||
"microsoft" \
|
||||
"https://packages.microsoft.com/keys/microsoft-2025.asc" \
|
||||
"https://packages.microsoft.com/debian/13/prod/" \
|
||||
"trixie" \
|
||||
"main"
|
||||
|
||||
if [[ -z "${var_dotnet_version:-}" && -t 0 ]]; then
|
||||
read -r -t 60 -p "${TAB3}.NET SDK version, 10 (LTS) or 11 [10]: " var_dotnet_version || echo
|
||||
fi
|
||||
var_dotnet_version="${var_dotnet_version:-10}"
|
||||
var_dotnet_version="${var_dotnet_version%%.*}"
|
||||
if ! apt-cache show "dotnet-sdk-${var_dotnet_version}.0" &>/dev/null; then
|
||||
msg_warn ".NET SDK ${var_dotnet_version} is not available for this system, using 10"
|
||||
var_dotnet_version="10"
|
||||
fi
|
||||
|
||||
if [[ -z "${var_project_name:-}" && -t 0 ]]; then
|
||||
read -r -t 60 -p "${TAB3}Assembly name of the project [testproject]: " var_project_name || echo
|
||||
fi
|
||||
var_project_name="${var_project_name:-testproject}"
|
||||
|
||||
msg_info "Installing .NET SDK ${var_dotnet_version}"
|
||||
$STD apt install -y "dotnet-sdk-${var_dotnet_version}.0"
|
||||
msg_ok "Installed .NET SDK ${var_dotnet_version}"
|
||||
|
||||
msg_info "Setting up FTP Server"
|
||||
useradd ftpuser
|
||||
@@ -55,7 +70,7 @@ msg_ok "FTP server setup completed"
|
||||
msg_info "Setting up Nginx Server"
|
||||
rm -f /var/www/html/index.nginx-debian.html
|
||||
|
||||
sed "s/\$var_project_name/$var_project_name/g" >myfile <<'EOF' >/etc/nginx/sites-available/default
|
||||
sed "s/\$var_project_name/$var_project_name/g" <<'EOF' >/etc/nginx/sites-available/default
|
||||
map $http_connection $connection_upgrade {
|
||||
"~*Upgrade" $http_connection;
|
||||
default keep-alive;
|
||||
@@ -82,12 +97,12 @@ msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/kestrel-aspnetapi.service
|
||||
[Unit]
|
||||
Description=.NET Web API App running on Linux
|
||||
ConditionPathExists=/var/www/html/$var_project_name.dll
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/var/www/html
|
||||
ExecStart=/usr/bin/dotnet /var/www/html/$var_project_name.dll
|
||||
Restart=always
|
||||
# Restart service after 10 seconds if the dotnet service crashes:
|
||||
RestartSec=10
|
||||
KillSignal=SIGINT
|
||||
SyslogIdentifier=dotnet-${var_project_name}
|
||||
@@ -98,7 +113,18 @@ Environment=DOTNET_NOLOGO=true
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now kestrel-aspnetapi
|
||||
cat <<EOF >/etc/systemd/system/kestrel-aspnetapi.path
|
||||
[Unit]
|
||||
Description=Start the .NET Web API App once it is deployed
|
||||
|
||||
[Path]
|
||||
PathExists=/var/www/html/$var_project_name.dll
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q kestrel-aspnetapi
|
||||
systemctl enable -q --now kestrel-aspnetapi.path
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Masked-Kunsiquat
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.espocrm.com/ | Github: https://github.com/espocrm/espocrm
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
if [[ -z "${var_admin_user:-}" ]]; then
|
||||
var_admin_user=$(prompt_input "EspoCRM admin username (a-z, 0-9, @ . _ -):" "admin" 60)
|
||||
fi
|
||||
var_admin_user="${var_admin_user:-admin}"
|
||||
var_admin_user="${var_admin_user,,}"
|
||||
if [[ ! "$var_admin_user" =~ ^[a-z0-9@._-]+$ ]]; then
|
||||
msg_warn "EspoCRM usernames take only a-z, 0-9 and @ . _ -; using admin"
|
||||
var_admin_user="admin"
|
||||
fi
|
||||
if [[ -z "${var_admin_pass:-}" ]]; then
|
||||
var_admin_pass=$(random_password 16)
|
||||
fi
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y nginx
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
setup_mariadb
|
||||
MARIADB_DB_NAME="espocrm" MARIADB_DB_USER="espocrm" setup_mariadb_db
|
||||
PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="ldap" setup_php
|
||||
fetch_and_deploy_gh_release "espo-crm" "espocrm/espocrm" "prebuild" "latest" "/opt/espocrm" "EspoCRM-*.zip"
|
||||
|
||||
msg_info "Configuring EspoCRM"
|
||||
cd /opt/espocrm
|
||||
$STD php bin/command config:populate
|
||||
$STD php bin/command config:set defaultPermissions.user www-data
|
||||
$STD php bin/command config:set defaultPermissions.group www-data
|
||||
$STD php bin/command config:set database.platform Mysql
|
||||
$STD php bin/command config:set database.host localhost
|
||||
$STD php bin/command config:set database.dbname "$MARIADB_DB_NAME"
|
||||
$STD php bin/command config:set database.user "$MARIADB_DB_USER"
|
||||
$STD php bin/command config:set database.password "$MARIADB_DB_PASS"
|
||||
$STD php bin/command rebuild
|
||||
$STD php bin/command create-admin-user "$var_admin_user"
|
||||
printf '%s\n' "$var_admin_pass" | $STD php bin/command set-password "$var_admin_user"
|
||||
$STD php bin/command config:set siteUrl "http://${LOCAL_IP}"
|
||||
$STD php bin/command populate-scheduled-jobs
|
||||
$STD php bin/command config:set jobRunInParallel true --type=bool
|
||||
$STD php bin/command app-check
|
||||
$STD php bin/command config:set isInstalled true --type=bool
|
||||
chown -R www-data:www-data /opt/espocrm
|
||||
msg_ok "Configured EspoCRM"
|
||||
|
||||
msg_info "Creating Nginx Site"
|
||||
PHP_SOCK=$(get_php_fpm_socket)
|
||||
cat <<EOF >/etc/nginx/sites-available/espocrm
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name _;
|
||||
|
||||
charset utf-8;
|
||||
index index.html index.php;
|
||||
|
||||
client_max_body_size 128M;
|
||||
keepalive_timeout 300;
|
||||
server_tokens off;
|
||||
fastcgi_send_timeout 300;
|
||||
fastcgi_read_timeout 300;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json;
|
||||
gzip_min_length 1000;
|
||||
|
||||
root /opt/espocrm/public;
|
||||
|
||||
location /client {
|
||||
root /opt/espocrm;
|
||||
autoindex off;
|
||||
|
||||
location ~* ^.+\.(js|css|png|jpg|svg|svgz|jpeg|gif|ico|tpl)\$ {
|
||||
access_log off;
|
||||
expires max;
|
||||
}
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
location ~ \.php\$ {
|
||||
fastcgi_pass unix:${PHP_SOCK};
|
||||
include fastcgi_params;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
|
||||
fastcgi_param QUERY_STRING \$query_string;
|
||||
}
|
||||
|
||||
location /api/v1/ {
|
||||
if (!-e \$request_filename) {
|
||||
rewrite ^/api/v1/(.*)\$ /api/v1/index.php last;
|
||||
}
|
||||
}
|
||||
|
||||
location /portal/ {
|
||||
try_files \$uri \$uri/ /portal/index.php?\$query_string;
|
||||
}
|
||||
|
||||
location /api/v1/portal-access {
|
||||
if (!-e \$request_filename) {
|
||||
rewrite ^/api/v1/(.*)\$ /api/v1/portal-access/index.php last;
|
||||
}
|
||||
}
|
||||
|
||||
location ~ /(\.htaccess|web\.config|\.git) {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
msg_ok "Created Nginx Site"
|
||||
nginx_enable_site "espocrm"
|
||||
|
||||
msg_info "Setting up Cron"
|
||||
cat <<EOF >/etc/cron.d/espocrm
|
||||
* * * * * www-data cd /opt/espocrm && php -f cron.php >/dev/null 2>&1
|
||||
EOF
|
||||
systemctl enable -q --now cron
|
||||
msg_ok "Set up Cron"
|
||||
|
||||
msg_info "Saving Credentials"
|
||||
cat <<EOF >>~/espocrm.creds
|
||||
|
||||
EspoCRM Admin
|
||||
Username: ${var_admin_user}
|
||||
Password: ${var_admin_pass}
|
||||
EOF
|
||||
chmod 600 ~/espocrm.creds
|
||||
msg_ok "Saved Credentials"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -26,6 +26,7 @@ chown -R www-data:www-data /opt/firefly
|
||||
chmod -R 775 /opt/firefly/storage
|
||||
cd /opt/firefly
|
||||
cp .env.example .env
|
||||
sed -i -e "s|^APP_KEY=.*|APP_KEY=$(random_password 32)|" -e "s|^STATIC_CRON_TOKEN=.*|STATIC_CRON_TOKEN=$(random_password 32)|" /opt/firefly/.env
|
||||
sed -i "s/DB_HOST=.*/DB_HOST=localhost/" /opt/firefly/.env
|
||||
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$MARIADB_DB_PASS/" /opt/firefly/.env
|
||||
$STD composer install --no-dev --no-plugins --no-interaction
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mozilla-services/syncstorage-rs
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libpq-dev \
|
||||
python3-dev
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_VERSION="18" setup_postgresql
|
||||
PG_DB_NAME="syncserver" PG_DB_USER="syncserver" setup_postgresql_db
|
||||
setup_rust
|
||||
setup_uv
|
||||
|
||||
fetch_and_deploy_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs" "tarball"
|
||||
|
||||
msg_info "Building Firefox Syncserver (Patience)"
|
||||
cd /opt/firefox-syncserver
|
||||
$STD cargo build --release --locked --no-default-features --features=syncstorage-db/postgres --features=tokenserver-db/postgres --features=py_verifier
|
||||
cp target/release/syncserver /usr/local/bin/syncserver
|
||||
rm -rf target
|
||||
$STD uv venv --python /usr/bin/python3 /opt/firefox-syncserver/.venv
|
||||
$STD uv pip install --python /opt/firefox-syncserver/.venv/bin/python pyfxa tokenlib cryptography
|
||||
msg_ok "Built Firefox Syncserver"
|
||||
|
||||
msg_info "Configuring Firefox Syncserver"
|
||||
mkdir -p /opt/firefox-syncserver_data
|
||||
cat <<EOF >/opt/firefox-syncserver_data/.env
|
||||
SYNC_HOST=0.0.0.0
|
||||
SYNC_PORT=8000
|
||||
SYNC_MASTER_SECRET=$(openssl rand -hex 32)
|
||||
SYNC_HUMAN_LOGS=true
|
||||
RUST_LOG=info
|
||||
SYNC_SYNCSTORAGE__DATABASE_URL=postgres://syncserver:${PG_DB_PASS}@127.0.0.1:5432/syncserver
|
||||
SYNC_SYNCSTORAGE__GLEAN_ENABLED=false
|
||||
SYNC_TOKENSERVER__ENABLED=true
|
||||
SYNC_TOKENSERVER__RUN_MIGRATIONS=true
|
||||
SYNC_TOKENSERVER__DATABASE_URL=postgres://syncserver:${PG_DB_PASS}@127.0.0.1:5432/syncserver
|
||||
SYNC_TOKENSERVER__NODE_TYPE=postgres
|
||||
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN=api.accounts.firefox.com
|
||||
SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL=https://oauth.accounts.firefox.com
|
||||
SYNC_TOKENSERVER__INIT_NODE_URL=http://${LOCAL_IP}:8000
|
||||
EOF
|
||||
chmod 600 /opt/firefox-syncserver_data/.env
|
||||
cat <<'EOF' >/opt/firefox-syncserver_data/nodes.sql
|
||||
SELECT to_regclass('nodes') IS NOT NULL AS ready \gset
|
||||
\if :ready
|
||||
UPDATE nodes SET node = :'url'
|
||||
WHERE id = (SELECT min(id) FROM nodes)
|
||||
AND NOT EXISTS (SELECT 1 FROM nodes WHERE node = :'url');
|
||||
UPDATE users SET nodeid = (SELECT id FROM nodes WHERE node = :'url')
|
||||
WHERE nodeid <> (SELECT id FROM nodes WHERE node = :'url');
|
||||
DELETE FROM nodes WHERE node <> :'url';
|
||||
\endif
|
||||
EOF
|
||||
msg_ok "Configured Firefox Syncserver"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/firefox-syncserver.service
|
||||
[Unit]
|
||||
Description=Firefox Syncserver
|
||||
After=network.target postgresql.service
|
||||
Wants=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/firefox-syncserver
|
||||
EnvironmentFile=/opt/firefox-syncserver_data/.env
|
||||
Environment=PYTHONPATH=/opt/firefox-syncserver/.venv/lib/python3.13/site-packages
|
||||
ExecStartPre=/usr/bin/psql -1 -q -v ON_ERROR_STOP=1 -v url=\${SYNC_TOKENSERVER__INIT_NODE_URL} -f /opt/firefox-syncserver_data/nodes.sql \${SYNC_TOKENSERVER__DATABASE_URL}
|
||||
ExecStart=/usr/local/bin/syncserver
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now firefox-syncserver
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -14,18 +14,15 @@ network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
CODENAME=$(get_os_info codename)
|
||||
setup_deb822_repo \
|
||||
"debian-backports" \
|
||||
"file:///usr/share/keyrings/debian-archive-keyring.gpg" \
|
||||
"http://deb.debian.org/debian" \
|
||||
"${CODENAME}-backports" \
|
||||
"main"
|
||||
$STD apt install -y \
|
||||
chromium \
|
||||
libreoffice-writer \
|
||||
libreoffice-calc \
|
||||
libreoffice-impress \
|
||||
libreoffice-draw \
|
||||
libreoffice-math \
|
||||
python3-uno \
|
||||
python-is-python3 \
|
||||
pdftk-java \
|
||||
qpdf \
|
||||
libimage-exiftool-perl \
|
||||
fonts-crosextra-carlito \
|
||||
fonts-crosextra-caladea \
|
||||
fonts-liberation \
|
||||
@@ -33,28 +30,30 @@ $STD apt install -y \
|
||||
fonts-dejavu \
|
||||
fonts-noto-cjk \
|
||||
fonts-noto-color-emoji \
|
||||
fonts-noto-core
|
||||
fonts-noto-core \
|
||||
fonts-urw-base35
|
||||
# lok is built against the LibreOffice from backports
|
||||
$STD apt install -y -t "${CODENAME}-backports" \
|
||||
libreoffice-writer \
|
||||
libreoffice-calc \
|
||||
libreoffice-impress \
|
||||
libreoffice-draw \
|
||||
libreoffice-math
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
fetch_and_deploy_gh_release "pdfcpu" "pdfcpu/pdfcpu" "prebuild" "latest" "/opt/pdfcpu" "pdfcpu_*_Linux_$(arch_resolve "x86_64" "arm64").tar.xz"
|
||||
ln -sf "$(find /opt/pdfcpu -type f -name pdfcpu | head -n1)" /usr/local/bin/pdfcpu
|
||||
|
||||
msg_info "Installing unoconverter"
|
||||
UNOCONVERTER_VERSION=$(get_latest_github_release "gotenberg/unoconverter" "false")
|
||||
download_file "https://raw.githubusercontent.com/gotenberg/unoconverter/${UNOCONVERTER_VERSION}/unoconv" /usr/local/bin/unoconverter
|
||||
chmod +x /usr/local/bin/unoconverter
|
||||
msg_ok "Installed unoconverter"
|
||||
|
||||
fetch_and_deploy_gh_release "gotenberg" "gotenberg/gotenberg" "tarball" "latest" "/opt/gotenberg"
|
||||
|
||||
GO_VERSION="$(awk '$1=="go"{print $2}' /opt/gotenberg/go.mod | cut -d. -f1,2)" setup_go
|
||||
|
||||
LOK_VERSION="$(awk -F= '/^ARG LOK_VERSION=/{print $2}' /opt/gotenberg/build/Dockerfile)"
|
||||
fetch_and_deploy_gh_release "lok" "gotenberg/lok" "prebuild" "$LOK_VERSION" "/usr/local/bin" "lok_${LOK_VERSION#v}_linux_$(arch_resolve "amd64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Building Gotenberg (Patience)"
|
||||
cd /opt/gotenberg
|
||||
export CGO_ENABLED=0
|
||||
$STD go mod download
|
||||
$STD go build -o /usr/local/bin/gotenberg \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v9/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
cmd/gotenberg/main.go
|
||||
msg_ok "Built Gotenberg"
|
||||
|
||||
@@ -70,14 +69,10 @@ Type=simple
|
||||
User=root
|
||||
Environment=LANG=C.UTF-8
|
||||
Environment=LC_ALL=C.UTF-8
|
||||
Environment=PDFTK_BIN_PATH=/usr/bin/pdftk
|
||||
Environment=QPDF_BIN_PATH=/usr/bin/qpdf
|
||||
Environment=EXIFTOOL_BIN_PATH=/usr/bin/exiftool
|
||||
Environment=PDFCPU_BIN_PATH=/usr/local/bin/pdfcpu
|
||||
Environment=CHROMIUM_BIN_PATH=/usr/bin/chromium
|
||||
Environment=CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/build/chromium-hyphen-data
|
||||
Environment=LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
|
||||
Environment=UNOCONVERTER_BIN_PATH=/usr/local/bin/unoconverter
|
||||
Environment=LOK_BIN_PATH=/usr/local/bin/lok
|
||||
Environment=OTEL_TRACES_EXPORTER=none
|
||||
Environment=OTEL_METRICS_EXPORTER=none
|
||||
Environment=OTEL_LOGS_EXPORTER=none
|
||||
|
||||
@@ -33,7 +33,13 @@ msg_ok "Set up Python Backend"
|
||||
msg_info "Configuring Homelable"
|
||||
mkdir -p /opt/homelable/data
|
||||
SECRET_KEY=$(openssl rand -hex 32)
|
||||
BCRYPT_HASH=$(/opt/homelable/backend/.venv/bin/python -c "import bcrypt; print(bcrypt.hashpw(b'admin', bcrypt.gensalt()).decode())")
|
||||
HOMELABLE_PASS=$(random_password 20)
|
||||
BCRYPT_HASH=$(/opt/homelable/backend/.venv/bin/python -c "import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt()).decode())" "$HOMELABLE_PASS")
|
||||
{
|
||||
echo "Homelable Credentials"
|
||||
echo "Username: admin"
|
||||
echo "Password: ${HOMELABLE_PASS}"
|
||||
} >~/homelable.creds
|
||||
cat <<EOF >/opt/homelable/backend/.env
|
||||
SECRET_KEY=${SECRET_KEY}
|
||||
SQLITE_PATH=/opt/homelable/data/homelab.db
|
||||
|
||||
@@ -23,7 +23,7 @@ if [[ "$(arch_resolve)" == "amd64" ]] && ! grep -qm1 sse4_2 /proc/cpuinfo; then
|
||||
exit 236
|
||||
fi
|
||||
|
||||
RELEASE="v3.3.6"
|
||||
RELEASE="v3.4.1"
|
||||
if lscpu | grep -q 'GenuineIntel'; then
|
||||
echo ""
|
||||
echo ""
|
||||
@@ -68,6 +68,7 @@ msg_info "Installing dependencies"
|
||||
$STD apt install --no-install-recommends -y \
|
||||
git \
|
||||
redis \
|
||||
pigz \
|
||||
autoconf \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
@@ -222,7 +223,7 @@ LIBJXL_REVISION="$(jq -cr '.revision' "$BASE_DIR"/server/sources/libjxl.json)"
|
||||
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
|
||||
cd "$SOURCE"
|
||||
$STD git reset --hard "$LIBJXL_REVISION"
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow -- third_party
|
||||
mkdir build
|
||||
cd build
|
||||
$STD cmake \
|
||||
@@ -415,7 +416,7 @@ cp -a web/build "$APP_DIR"/www
|
||||
cp LICENSE "$APP_DIR"
|
||||
cd "$SRC_DIR"
|
||||
export MISE_TRUSTED_CONFIG_PATHS="$SRC_DIR"/mise.toml
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,npm:@openapitools/openapi-generator-cli
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,restic,npm:@openapitools/openapi-generator-cli
|
||||
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
|
||||
mise_ok=0
|
||||
for i in 1 2 3; do
|
||||
@@ -433,7 +434,8 @@ if ! command -v extism-js >/dev/null 2>&1; then
|
||||
# fetch_and_deploy_gh_release cannot deploy (singlefile leaves it compressed,
|
||||
# prebuild only handles zip/tar). Fetch + gunzip it directly.
|
||||
EXTISM_ARCH="$(arch_resolve x86_64 aarch64)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-${EXTISM_ARCH}-linux-v1.6.0.gz"
|
||||
EXTISM_VERSION="$(grep -oP '"github:extism/js-pdk"\s*=\s*"\K[^"]+' "$SRC_DIR"/mise.toml)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/${EXTISM_VERSION}/extism-js-${EXTISM_ARCH}-linux-${EXTISM_VERSION}.gz"
|
||||
gunzip -f /tmp/extism-js.gz
|
||||
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
|
||||
rm -f /tmp/extism-js
|
||||
@@ -445,6 +447,25 @@ cp -r ./packages/plugin-core/dist "$PLUGIN_DIR"/dist
|
||||
cp ./packages/plugin-core/manifest.json "$PLUGIN_DIR"
|
||||
msg_ok "Installed Immich Server, Web and Plugin Components"
|
||||
|
||||
RESTIC_REF="$(grep -oP 'futo-org/restic:sha-\K[0-9a-f]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
RESTIC_PROXY_REF="$(grep -oP 'futo-org/backups/restic-proxy:\K[^@]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
FUTO_DIR=/opt/staging/futo
|
||||
rm -rf "$FUTO_DIR"
|
||||
$STD git clone --filter=blob:none --no-checkout https://github.com/futo-org/restic "$FUTO_DIR"/restic
|
||||
$STD git -C "$FUTO_DIR"/restic checkout "$RESTIC_REF"
|
||||
$STD git clone --depth 1 --filter=blob:none --sparse -b "$RESTIC_PROXY_REF" https://github.com/futo-org/backups "$FUTO_DIR"/backups
|
||||
$STD git -C "$FUTO_DIR"/backups sparse-checkout set packages/restic-proxy
|
||||
GO_VERSION="$(awk '/^go /{print $2}' "$FUTO_DIR"/backups/packages/restic-proxy/go.mod)" setup_go
|
||||
msg_info "Building restic for FUTO Backups"
|
||||
cd "$FUTO_DIR"/restic
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic ./cmd/restic
|
||||
cd "$FUTO_DIR"/backups/packages/restic-proxy
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic-proxy .
|
||||
cd "$SRC_DIR"
|
||||
rm -rf "$FUTO_DIR"
|
||||
echo "${RESTIC_REF} ${RESTIC_PROXY_REF}" >~/.immich_restic
|
||||
msg_ok "Built restic for FUTO Backups"
|
||||
|
||||
cd "$SRC_DIR"/machine-learning
|
||||
$STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich
|
||||
mkdir -p "$ML_DIR"
|
||||
|
||||
@@ -21,7 +21,7 @@ msg_ok "Installed Dependencies"
|
||||
|
||||
PG_VERSION="16" setup_postgresql
|
||||
PG_DB_NAME="kan" PG_DB_USER="kan" setup_postgresql_db
|
||||
NODE_VERSION="20" NODE_MODULE="pnpm" setup_nodejs
|
||||
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
|
||||
|
||||
fetch_and_deploy_gh_tag "kan" "kanbn/kan" "latest"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ PG_DB_NAME="keycloak" PG_DB_USER="keycloak" setup_postgresql_db
|
||||
fetch_and_deploy_gh_release "keycloak_app" "keycloak/keycloak" "prebuild" "latest" "/opt/keycloak" "keycloak-*.tar.gz"
|
||||
|
||||
msg_info "Creating Service"
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
cat <<EOF >/etc/systemd/system/keycloak.service
|
||||
[Unit]
|
||||
Description=Keycloak Service
|
||||
@@ -40,7 +41,7 @@ Environment="KC_DB_USERNAME=keycloak"
|
||||
Environment="KC_DB_PASSWORD=$PG_DB_PASS"
|
||||
Environment="KC_HTTP_ENABLED=true"
|
||||
Environment="KC_BOOTSTRAP_ADMIN_USERNAME=tmpadm"
|
||||
Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=admin123"
|
||||
Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASS}"
|
||||
# Comment following line and uncomment the next 2 if working behind a reverse proxy
|
||||
Environment="KC_HOSTNAME_STRICT=false"
|
||||
#Environment="KC_HOSTNAME=keycloak.example.com"
|
||||
@@ -48,6 +49,13 @@ Environment="KC_HOSTNAME_STRICT=false"
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 600 /etc/systemd/system/keycloak.service
|
||||
{
|
||||
echo "Keycloak Credentials"
|
||||
echo "Temporary admin, replace it with a permanent one"
|
||||
echo "Username: tmpadm"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/keycloak.creds
|
||||
systemctl enable -q --now keycloak
|
||||
msg_ok "Created Service"
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ cp .env.dist .env
|
||||
sed -i "/^DATABASE_URL=.*/c\DATABASE_URL=mysql://kimai:$MARIADB_DB_PASS@127.0.0.1:3306/kimai_db?charset=utf8mb4&serverVersion=mariadb-$MYSQL_VERSION" /opt/kimai/.env
|
||||
sed -i "s|^APP_SECRET=.*|APP_SECRET=$APP_SECRET|" /opt/kimai/.env
|
||||
$STD bin/console kimai:install -n
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
$STD expect <<EOF
|
||||
set timeout -1
|
||||
log_user 0
|
||||
@@ -47,10 +48,16 @@ log_user 0
|
||||
spawn bin/console kimai:user:create admin admin@community-scripts.org ROLE_SUPER_ADMIN
|
||||
|
||||
expect "Please enter the password:"
|
||||
send "community-scripts.org\r"
|
||||
send "${ADMIN_PASS}\r"
|
||||
|
||||
expect eof
|
||||
EOF
|
||||
{
|
||||
echo "Kimai Credentials"
|
||||
echo "Username: admin"
|
||||
echo "Email: admin@community-scripts.org"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/kimai.creds
|
||||
$STD composer update --no-interaction
|
||||
cat <<EOF >/opt/kimai/config/packages/local.yaml
|
||||
kimai:
|
||||
|
||||
@@ -33,7 +33,13 @@ $STD yarn config set ignore-engines true
|
||||
$STD yarn install
|
||||
$STD yarn run production
|
||||
$STD php artisan key:generate
|
||||
$STD php artisan setup:production --email=admin@community-scripts.org --password=community-scripts.org --force
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
$STD php artisan setup:production --email=admin@community-scripts.org --password="${ADMIN_PASS}" --force
|
||||
{
|
||||
echo "Monica Credentials"
|
||||
echo "Email: admin@community-scripts.org"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/monica.creds
|
||||
chown -R www-data:www-data /opt/monica
|
||||
chmod -R 775 /opt/monica/storage
|
||||
echo "* * * * * root php /opt/monica/artisan schedule:run >> /dev/null 2>&1" >>/etc/crontab
|
||||
|
||||
@@ -13,7 +13,7 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
fetch_and_deploy_gh_release "myip" "jason5ng32/MyIP" "tarball"
|
||||
|
||||
msg_info "Configuring MyIP"
|
||||
|
||||
@@ -63,7 +63,13 @@ setcap cap_net_raw+p /bin/ping
|
||||
msg_ok "Built Nagios Plugins"
|
||||
|
||||
msg_info "Configuring Web Authentication"
|
||||
$STD htpasswd -bc /usr/local/nagios/etc/htpasswd.users nagiosadmin nagiosadmin
|
||||
NAGIOS_PASS=$(random_password 20)
|
||||
$STD htpasswd -bc /usr/local/nagios/etc/htpasswd.users nagiosadmin "$NAGIOS_PASS"
|
||||
{
|
||||
echo "Nagios Credentials"
|
||||
echo "Username: nagiosadmin"
|
||||
echo "Password: ${NAGIOS_PASS}"
|
||||
} >~/nagios.creds
|
||||
chown root:www-data /usr/local/nagios/etc/htpasswd.users
|
||||
chmod 640 /usr/local/nagios/etc/htpasswd.users
|
||||
msg_ok "Configured Web Authentication"
|
||||
|
||||
@@ -201,6 +201,13 @@ cat <<'EOF' >/etc/neko/openbox.xml
|
||||
</openbox_config>
|
||||
EOF
|
||||
|
||||
NEKO_ADMIN_PASS=$(random_password 20)
|
||||
NEKO_USER_PASS=$(random_password 20)
|
||||
{
|
||||
echo "Neko Credentials"
|
||||
echo "Admin password: ${NEKO_ADMIN_PASS}"
|
||||
echo "User password: ${NEKO_USER_PASS}"
|
||||
} >~/neko.creds
|
||||
cat <<EOF >/etc/neko/neko.yaml
|
||||
server:
|
||||
bind: "0.0.0.0:8080"
|
||||
@@ -219,8 +226,8 @@ desktop:
|
||||
member:
|
||||
provider: "multiuser"
|
||||
multiuser:
|
||||
admin_password: "admin"
|
||||
user_password: "neko"
|
||||
admin_password: "${NEKO_ADMIN_PASS}"
|
||||
user_password: "${NEKO_USER_PASS}"
|
||||
EOF
|
||||
msg_ok "Set up Runtime"
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ NODEBB_USER="nodebb"
|
||||
NODEBB_PWD="$(random_password 13)"
|
||||
MONGO_CONNECTION_STRING="mongodb://${NODEBB_USER}:${NODEBB_PWD}@localhost:27017/nodebb"
|
||||
NODEBB_SECRET=$(uuidgen)
|
||||
NODEBB_ADMIN_PASS="$(random_password 20)"
|
||||
cat <<EOF >~/nodebb.creds
|
||||
NodeBB-Credentials
|
||||
Mongo Database User: $MONGO_ADMIN_USER
|
||||
@@ -38,6 +39,8 @@ Mongo Database Password: $MONGO_ADMIN_PWD
|
||||
NodeBB User: $NODEBB_USER
|
||||
NodeBB Password: $NODEBB_PWD
|
||||
NodeBB Secret: $NODEBB_SECRET
|
||||
NodeBB Admin User: community-scripts
|
||||
NodeBB Admin Password: $NODEBB_ADMIN_PASS
|
||||
EOF
|
||||
|
||||
$STD mongosh <<EOF
|
||||
@@ -97,10 +100,10 @@ expect "Administrator email address" {
|
||||
send "admin@community-scripts.org\r"
|
||||
}
|
||||
expect "Password" {
|
||||
send "community-scripts\r"
|
||||
send "$NODEBB_ADMIN_PASS\r"
|
||||
}
|
||||
expect "Confirm Password" {
|
||||
send "community-scripts\r"
|
||||
send "$NODEBB_ADMIN_PASS\r"
|
||||
}
|
||||
expect eof
|
||||
EOF
|
||||
|
||||
@@ -57,11 +57,17 @@ done
|
||||
ACME_EMAIL_INPUT="${var_npmplus_acme_email:-}"
|
||||
[[ -n "$ACME_EMAIL_INPUT" ]] || read -r -p "${TAB3}Enter your ACME Email (optional, press ENTER to skip): " ACME_EMAIL_INPUT || true
|
||||
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
yq -i "
|
||||
.services.npmplus.environment |=
|
||||
(map(select(. != \"TZ=*\" and . != \"ACME_EMAIL=*\" and . != \"INITIAL_ADMIN_EMAIL=*\" and . != \"INITIAL_ADMIN_PASSWORD=*\")) +
|
||||
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=community-scripts.org\"])
|
||||
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=${ADMIN_PASS}\"])
|
||||
" /opt/compose.yaml
|
||||
{
|
||||
echo "NPMplus Credentials"
|
||||
echo "Email: admin@local.com"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/npmplus.creds
|
||||
|
||||
msg_info "Building and Starting NPMplus (Patience)"
|
||||
$STD docker compose up -d
|
||||
|
||||
@@ -30,7 +30,13 @@ rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
|
||||
msg_ok "Setup Python3"
|
||||
|
||||
msg_info "Creating user octoprint"
|
||||
useradd -m -s /bin/bash -p $(openssl passwd -1 octoprint) octoprint
|
||||
OCTOPRINT_PASS=$(random_password 20)
|
||||
useradd -m -s /bin/bash -p "$(openssl passwd -6 "$OCTOPRINT_PASS")" octoprint
|
||||
{
|
||||
echo "OctoPrint Linux user Credentials"
|
||||
echo "Username: octoprint"
|
||||
echo "Password: ${OCTOPRINT_PASS}"
|
||||
} >~/octoprint.creds
|
||||
usermod -aG sudo,tty,dialout octoprint
|
||||
chown -R octoprint:octoprint /opt
|
||||
echo "octoprint ALL=NOPASSWD: $(command -v systemctl) restart octoprint, $(command -v reboot), $(command -v poweroff)" >/etc/sudoers.d/octoprint
|
||||
|
||||
@@ -16,7 +16,7 @@ update_os
|
||||
msg_info "Installing OpenObserve"
|
||||
mkdir -p /opt/openobserve/data
|
||||
RELEASE=$(get_latest_github_release "openobserve/openobserve")
|
||||
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
|
||||
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve --no-same-owner
|
||||
ROOT_PASS="$(random_password 9)Aa1!"
|
||||
|
||||
cat <<EOF >/opt/openobserve/data/.env
|
||||
@@ -25,7 +25,7 @@ ZO_ROOT_USER_PASSWORD = "${ROOT_PASS}"
|
||||
ZO_DATA_DIR = "/opt/openobserve/data"
|
||||
ZO_HTTP_PORT = "5080"
|
||||
EOF
|
||||
echo "${RELEASE}" >>~/.openobserve
|
||||
echo "${RELEASE}" >~/.openobserve
|
||||
msg_ok "Installed OpenObserve"
|
||||
|
||||
msg_info "Creating Service"
|
||||
@@ -38,7 +38,6 @@ After=network.target
|
||||
Type=simple
|
||||
EnvironmentFile=/opt/openobserve/data/.env
|
||||
ExecStart=/opt/openobserve/openobserve
|
||||
ExecStop=killall -QUIT openobserve
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -70,6 +70,9 @@ memcached/autoinstall install
|
||||
openproject/admin_email admin@example.net
|
||||
openproject/default_language en
|
||||
EOF
|
||||
# Debian's memcached also listens on ::1 and does not start when IPv6 is disabled
|
||||
$STD apt install -y memcached
|
||||
sed -i '/^-l ::1$/d' /etc/memcached.conf
|
||||
$STD sudo openproject configure
|
||||
systemctl stop openproject-web-1
|
||||
if ! grep -qF 'Environment=LD_PRELOAD=/usr/local/lib/libjemalloc.so.2' /etc/systemd/system/openproject-web-1.service; then
|
||||
|
||||
@@ -13,100 +13,36 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
build-essential
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Installing Python3"
|
||||
$STD apt install -y \
|
||||
python3-pip \
|
||||
python3-dev \
|
||||
python3-venv
|
||||
mkdir -p ~/.config/pip
|
||||
cat >~/.config/pip/pip.conf <<EOF
|
||||
[global]
|
||||
break-system-packages = true
|
||||
EOF
|
||||
msg_ok "Installed Python3"
|
||||
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
|
||||
|
||||
msg_info "Setup Paperless-AI"
|
||||
cd /opt/paperless-ai
|
||||
$STD python3 -m venv /opt/paperless-ai/venv
|
||||
source /opt/paperless-ai/venv/bin/activate
|
||||
# TMPDIR to use container disk instead of tmpfs for large pip downloads (https://github.com/community-scripts/ProxmoxVE/issues/10338)
|
||||
export TMPDIR=/opt/paperless-ai/tmp
|
||||
mkdir -p "$TMPDIR"
|
||||
$STD pip install --upgrade pip
|
||||
$STD pip install --no-cache-dir -r requirements.txt
|
||||
rm -rf "$TMPDIR"
|
||||
mkdir -p data/chromadb
|
||||
$STD npm ci --only=production
|
||||
export ONNXRUNTIME_NODE_INSTALL=skip PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
$STD npm ci --ignore-scripts --no-audit --no-fund
|
||||
$STD npm run build
|
||||
$STD npm prune --omit=dev --ignore-scripts --no-audit --no-fund
|
||||
mkdir -p /opt/paperless-ai/data
|
||||
cat <<EOF >/opt/paperless-ai/data/.env
|
||||
PAPERLESS_API_URL=
|
||||
PAPERLESS_API_TOKEN=
|
||||
PAPERLESS_USERNAME=
|
||||
AI_PROVIDER=openai
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_MODEL=gpt-4o-mini
|
||||
OLLAMA_API_URL=
|
||||
OLLAMA_MODEL=
|
||||
SCAN_INTERVAL=*/10 * * * *
|
||||
SYSTEM_PROMPT=""
|
||||
PROCESS_PREDEFINED_DOCUMENTS=no
|
||||
TAGS=
|
||||
ADD_AI_PROCESSED_TAG=no
|
||||
AI_PROCESSED_TAG_NAME=ki-gen
|
||||
USE_PROMPT_TAGS=no
|
||||
PROMPT_TAGS=
|
||||
USE_EXISTING_DATA=no
|
||||
API_KEY=
|
||||
CUSTOM_API_KEY=
|
||||
CUSTOM_BASE_URL=
|
||||
CUSTOM_MODEL=
|
||||
RAG_SERVICE_URL=http://localhost:8000
|
||||
RAG_SERVICE_ENABLED=true
|
||||
EOF
|
||||
msg_ok "Setup Paperless-AI"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/paperless-ai.service
|
||||
[Unit]
|
||||
Description=PaperlessAI Service
|
||||
After=network.target paperless-rag.service
|
||||
Requires=paperless-rag.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/paperless-ai
|
||||
Environment="NODE_ENV=production"
|
||||
EnvironmentFile=/opt/paperless-ai/data/.env
|
||||
ExecStart=/usr/bin/node server.js
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat <<EOF >/etc/systemd/system/paperless-rag.service
|
||||
[Unit]
|
||||
Description=PaperlessAI-RAG Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/paperless-ai
|
||||
EnvironmentFile=/opt/paperless-ai/data/.env
|
||||
ExecStart=/opt/paperless-ai/venv/bin/python3 main.py --host 0.0.0.0 --port 8000 --initialize
|
||||
Environment="NODE_ENV=production"
|
||||
Environment="PAPERLESS_AI_PORT=3000"
|
||||
Environment="PAPERLESS_AI_DATA_DIR=/opt/paperless-ai/data"
|
||||
Environment="HF_HUB_DISABLE_TELEMETRY=1"
|
||||
ExecStart=/usr/bin/node dist/server/index.js
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now paperless-rag
|
||||
sleep 5
|
||||
systemctl enable -q --now paperless-ai
|
||||
msg_ok "Created Service"
|
||||
|
||||
|
||||
@@ -43,7 +43,13 @@ $STD php artisan migrate --force --seed
|
||||
msg_ok "Set up database"
|
||||
|
||||
msg_info "Creating Admin User"
|
||||
$STD php artisan app:user:create paymenter admin admin@paymenter.org paymenter 1 -q
|
||||
PAYMENTER_PASS=$(random_password 20)
|
||||
$STD php artisan app:user:create paymenter admin admin@paymenter.org "$PAYMENTER_PASS" 1 -q
|
||||
{
|
||||
echo "Paymenter Credentials"
|
||||
echo "Email: admin@paymenter.org"
|
||||
echo "Password: ${PAYMENTER_PASS}"
|
||||
} >~/paymenter.creds
|
||||
msg_ok "Created Admin User"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
|
||||
@@ -46,10 +46,16 @@ tar -xzf /tmp/libheif.tar.gz -C /usr/local
|
||||
ldconfig
|
||||
echo "${LIBHEIF_URL}" >~/.photoprism_libheif
|
||||
chmod -R 755 /opt/photoprism/photos/originals
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
{
|
||||
echo "PhotoPrism Credentials"
|
||||
echo "Username: admin"
|
||||
echo "Password: ${ADMIN_PASS}"
|
||||
} >~/photoprism.creds
|
||||
cat <<EOF >/opt/photoprism/config/.env
|
||||
# Authentication
|
||||
PHOTOPRISM_ADMIN_USER='admin'
|
||||
PHOTOPRISM_ADMIN_PASSWORD='changeme'
|
||||
PHOTOPRISM_ADMIN_PASSWORD='${ADMIN_PASS}'
|
||||
PHOTOPRISM_AUTH_MODE='password'
|
||||
PHOTOPRISM_PUBLIC='false'
|
||||
|
||||
@@ -112,7 +118,7 @@ StoragePath: "/opt/photoprism/storage"
|
||||
OriginalsPath: "/opt/photoprism/photos/originals"
|
||||
ImportPath: "/media"
|
||||
AdminUser: "admin"
|
||||
AdminPassword: "changeme"
|
||||
AdminPassword: "${ADMIN_PASS}"
|
||||
AuthMode: "password"
|
||||
DatabaseDriver: "sqlite"
|
||||
HttpHost: "0.0.0.0"
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://pixelfed.org/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
nginx \
|
||||
redis-server \
|
||||
ffmpeg \
|
||||
jpegoptim \
|
||||
optipng \
|
||||
pngquant \
|
||||
gifsicle \
|
||||
libvips42
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Creating Pixelfed User"
|
||||
useradd -rU -s /bin/bash pixelfed
|
||||
usermod -aG redis pixelfed
|
||||
msg_ok "Created Pixelfed User"
|
||||
|
||||
PG_VERSION="17" setup_postgresql
|
||||
PG_DB_NAME="pixelfed" PG_DB_USER="pixelfed" setup_postgresql_db
|
||||
PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="bcmath,ctype,curl,exif,gd,imagick,intl,mbstring,pgsql,redis,xml,zip" PHP_UPLOAD_MAX_FILESIZE="500M" PHP_POST_MAX_SIZE="500M" PHP_MAX_EXECUTION_TIME="600" setup_php
|
||||
setup_composer
|
||||
|
||||
msg_info "Configuring Redis"
|
||||
REDIS_PASS=$(random_password)
|
||||
sed -i "s/^# requirepass foobared/requirepass $REDIS_PASS/" /etc/redis/redis.conf
|
||||
sed -i "s/^requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf
|
||||
systemctl restart redis-server
|
||||
msg_ok "Configured Redis"
|
||||
|
||||
msg_info "Configuring PHP-FPM Pool"
|
||||
cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's/\[www\]/[pixelfed]/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's/^user = www-data/user = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's/^group = www-data/group = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's|^listen = .*|listen = /run/php/php8.4-fpm-pixelfed.sock|' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's/^listen.owner = .*/listen.owner = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i 's/^listen.group = .*/listen.group = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
|
||||
sed -i -E 's/^\s*;?\s*ffi\.enable\s*=.*/ffi.enable=true/' /etc/php/8.4/fpm/php.ini
|
||||
systemctl restart php8.4-fpm
|
||||
msg_ok "Configured PHP-FPM Pool"
|
||||
|
||||
fetch_and_deploy_gh_release "pixelfed" "pixelfed/pixelfed" "tarball" "latest" "/opt/pixelfed"
|
||||
|
||||
msg_info "Installing Pixelfed (Patience)"
|
||||
cd /opt/pixelfed
|
||||
cat <<EOF >/opt/pixelfed/.env
|
||||
APP_NAME="Pixelfed"
|
||||
APP_ENV="production"
|
||||
APP_KEY=
|
||||
APP_DEBUG="false"
|
||||
APP_URL=http://${LOCAL_IP}
|
||||
APP_DOMAIN=${LOCAL_IP}
|
||||
ADMIN_DOMAIN=${LOCAL_IP}
|
||||
SESSION_DOMAIN=null
|
||||
TRUST_PROXIES="*"
|
||||
FORCE_HTTPS_URLS="false"
|
||||
|
||||
OPEN_REGISTRATION="false"
|
||||
ENFORCE_EMAIL_VERIFICATION="false"
|
||||
PF_MAX_USERS="1000"
|
||||
OAUTH_ENABLED="true"
|
||||
ENABLE_CONFIG_CACHE="true"
|
||||
INSTANCE_DISCOVER_PUBLIC="true"
|
||||
|
||||
PF_OPTIMIZE_IMAGES="true"
|
||||
IMAGE_QUALITY="80"
|
||||
MAX_PHOTO_SIZE="15000"
|
||||
MAX_CAPTION_LENGTH="500"
|
||||
MAX_ALBUM_LENGTH="4"
|
||||
|
||||
DB_CONNECTION="pgsql"
|
||||
DB_HOST="127.0.0.1"
|
||||
DB_PORT="5432"
|
||||
DB_DATABASE="${PG_DB_NAME}"
|
||||
DB_USERNAME="${PG_DB_USER}"
|
||||
DB_PASSWORD="${PG_DB_PASS}"
|
||||
|
||||
REDIS_CLIENT="predis"
|
||||
REDIS_SCHEME="tcp"
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PASSWORD="${REDIS_PASS}"
|
||||
REDIS_PORT="6379"
|
||||
|
||||
SESSION_DRIVER="database"
|
||||
CACHE_STORE="redis"
|
||||
QUEUE_DRIVER="redis"
|
||||
BROADCAST_DRIVER="log"
|
||||
LOG_CHANNEL="stack"
|
||||
HORIZON_PREFIX="horizon-"
|
||||
|
||||
ACTIVITY_PUB="true"
|
||||
AP_REMOTE_FOLLOW="true"
|
||||
AP_INBOX="true"
|
||||
AP_OUTBOX="true"
|
||||
AP_SHAREDINBOX="true"
|
||||
|
||||
EXP_EMC="true"
|
||||
|
||||
MAIL_DRIVER="log"
|
||||
MAIL_HOST="smtp.mailtrap.io"
|
||||
MAIL_PORT="2525"
|
||||
MAIL_USERNAME="null"
|
||||
MAIL_PASSWORD="null"
|
||||
MAIL_ENCRYPTION="null"
|
||||
MAIL_FROM_ADDRESS="pixelfed@example.com"
|
||||
MAIL_FROM_NAME="Pixelfed"
|
||||
|
||||
PF_ENABLE_CLOUD="false"
|
||||
FILESYSTEM_CLOUD="s3"
|
||||
SESSION_SECURE_COOKIE="false"
|
||||
HTTPS="false"
|
||||
EOF
|
||||
chmod -R 755 /opt/pixelfed
|
||||
chmod -R 775 /opt/pixelfed/storage /opt/pixelfed/bootstrap/cache
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
$STD composer install --no-dev --no-ansi --no-interaction --optimize-autoloader
|
||||
$STD php artisan key:generate --force
|
||||
$STD php artisan storage:link
|
||||
$STD php artisan migrate --force
|
||||
$STD php artisan import:cities
|
||||
$STD php artisan passport:keys
|
||||
$STD php artisan route:cache
|
||||
$STD php artisan view:cache
|
||||
$STD php artisan config:cache
|
||||
$STD php artisan instance:actor
|
||||
$STD php artisan horizon:install
|
||||
chown -R pixelfed:pixelfed /opt/pixelfed
|
||||
msg_ok "Installed Pixelfed"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
cat <<'EOF' >/etc/nginx/sites-available/pixelfed
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /opt/pixelfed/public;
|
||||
index index.php;
|
||||
|
||||
charset utf-8;
|
||||
client_max_body_size 100M;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm-pixelfed.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.(?!well-known).* {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
nginx_enable_site pixelfed
|
||||
msg_ok "Configured Nginx"
|
||||
|
||||
msg_info "Creating Services"
|
||||
cat <<'EOF' >/etc/systemd/system/pixelfed-horizon.service
|
||||
[Unit]
|
||||
Description=Pixelfed Horizon Queue Worker
|
||||
After=network.target redis-server.service postgresql.service
|
||||
Requires=redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pixelfed
|
||||
WorkingDirectory=/opt/pixelfed
|
||||
ExecStart=/usr/bin/php /opt/pixelfed/artisan horizon
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat <<'EOF' >/etc/systemd/system/pixelfed-scheduler.service
|
||||
[Unit]
|
||||
Description=Pixelfed Task Scheduler
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=pixelfed
|
||||
WorkingDirectory=/opt/pixelfed
|
||||
ExecStart=/usr/bin/php /opt/pixelfed/artisan schedule:run
|
||||
EOF
|
||||
|
||||
cat <<'EOF' >/etc/systemd/system/pixelfed-scheduler.timer
|
||||
[Unit]
|
||||
Description=Run Pixelfed Scheduler every minute
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* *:*:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
systemctl enable -q --now pixelfed-horizon pixelfed-scheduler.timer
|
||||
msg_ok "Created Services"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
@@ -3,7 +3,7 @@
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
|
||||
# Source: https://pocketbase.io/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
@@ -13,7 +13,9 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
fetch_and_deploy_gh_release "pocketbase" "pocketbase/pocketbase" "prebuild" "latest" "/opt/pocketbase" "pocketbase*linux_$(arch_resolve).zip"
|
||||
RELEASE=$(curl_with_retry "https://ganigeorgiev.com/api/collections/releases/records?filter=channel.repository.name%3D%22pocketbase%22%20%26%26%20channel.name%3D%22default%22&sort=-semver&skipTotal=1&perPage=1" "-" | jq -r '.items[0].tag' | sed 's/^v//')
|
||||
fetch_and_deploy_from_url "https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v${RELEASE}/pocketbase_${RELEASE}_linux_$(arch_resolve).zip" "/opt/pocketbase"
|
||||
echo "${RELEASE}" >~/.pocketbase
|
||||
|
||||
msg_info "Configuring Pocketbase"
|
||||
mkdir -p /opt/pocketbase/{pb_public,pb_migrations,pb_hooks}
|
||||
|
||||
@@ -18,12 +18,20 @@ fetch_and_deploy_gh_release "qbittorrent" "userdocs/qbittorrent-nox-static" "sin
|
||||
msg_info "Setup qBittorrent-nox"
|
||||
mv /opt/qbittorrent/qbittorrent /opt/qbittorrent/qbittorrent-nox
|
||||
mkdir -p ~/.config/qBittorrent/
|
||||
QB_PASS=$(random_password 20)
|
||||
QB_SALT=$(random_password 16)
|
||||
QB_HASH="$(printf '%s' "$QB_SALT" | base64 -w0):$(openssl kdf -keylen 64 -kdfopt digest:SHA512 -kdfopt pass:"$QB_PASS" -kdfopt salt:"$QB_SALT" -kdfopt iter:100000 -binary PBKDF2 | base64 -w0)"
|
||||
{
|
||||
echo "qBittorrent Credentials"
|
||||
echo "Username: admin"
|
||||
echo "Password: ${QB_PASS}"
|
||||
} >~/qbittorrent.creds
|
||||
cat <<EOF >~/.config/qBittorrent/qBittorrent.conf
|
||||
[LegalNotice]
|
||||
Accepted=true
|
||||
|
||||
[Preferences]
|
||||
WebUI\Password_PBKDF2="@ByteArray(amjeuVrF3xRbgzqWQmes5A==:XK3/Ra9jUmqUc4RwzCtrhrkQIcYczBl90DJw2rT8DFVTss4nxpoRhvyxhCf87ahVE3SzD8K9lyPdpyUCfmVsUg==)"
|
||||
WebUI\Password_PBKDF2="@ByteArray(${QB_HASH})"
|
||||
WebUI\Port=8090
|
||||
WebUI\UseUPnP=false
|
||||
WebUI\Username=admin
|
||||
|
||||
@@ -43,9 +43,15 @@ $STD rabbitmqctl enable_feature_flag all
|
||||
msg_ok "Enabled RabbitMQ Management Plugin"
|
||||
|
||||
msg_info "Creating User"
|
||||
$STD rabbitmqctl add_user proxmox proxmox
|
||||
RABBITMQ_PASS=$(random_password 20)
|
||||
$STD rabbitmqctl add_user proxmox "$RABBITMQ_PASS"
|
||||
$STD rabbitmqctl set_user_tags proxmox administrator
|
||||
$STD rabbitmqctl set_permissions -p / proxmox ".*" ".*" ".*"
|
||||
{
|
||||
echo "RabbitMQ Credentials"
|
||||
echo "Username: proxmox"
|
||||
echo "Password: ${RABBITMQ_PASS}"
|
||||
} >~/rabbitmq.creds
|
||||
msg_ok "Created User"
|
||||
|
||||
motd_ssh
|
||||
|
||||
+19
-8
@@ -257,15 +257,33 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# COEP/COOP for the EmulatorJS and js-dos players (SharedArrayBuffer needs a
|
||||
# crossOriginIsolated document). Matched on $request_uri because try_files
|
||||
# rewrites $uri to /index.html before add_header runs, so a per-location
|
||||
# add_header on the player route never fires. Mirrors upstream RomM's
|
||||
# docker/nginx/templates/default.conf.template.
|
||||
map $request_uri $coep_header {
|
||||
default "";
|
||||
~^/rom/.*/(ejs|jsdos)(\?|$) "require-corp";
|
||||
}
|
||||
map $request_uri $coop_header {
|
||||
default "";
|
||||
~^/rom/.*/(ejs|jsdos)(\?|$) "same-origin";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /opt/romm/frontend/dist;
|
||||
client_max_body_size 0;
|
||||
|
||||
# Frontend SPA
|
||||
# Frontend SPA - also emits the player isolation headers: the player URLs
|
||||
# fall through try_files to /index.html, which this location serves
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache";
|
||||
add_header Cross-Origin-Embedder-Policy $coep_header;
|
||||
add_header Cross-Origin-Opener-Policy $coop_header;
|
||||
}
|
||||
|
||||
# Static assets
|
||||
@@ -276,13 +294,6 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# EmulatorJS player - requires COOP/COEP headers for SharedArrayBuffer
|
||||
location ~ ^/rom/.*/ejs$ {
|
||||
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||||
add_header Cross-Origin-Opener-Policy "same-origin";
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# Backend API
|
||||
location /api {
|
||||
proxy_pass http://romm_backend;
|
||||
|
||||
@@ -151,7 +151,7 @@ INVITE_EMAIL_TEMPLATE=/opt/signoz/templates/invitation_email_template.html
|
||||
SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
SIGNOZ_WEB_ENABLED=true
|
||||
SIGNOZ_WEB_DIRECTORY=/opt/signoz/web
|
||||
SIGNOZ_JWT_SECRET=secret
|
||||
SIGNOZ_JWT_SECRET=$(openssl rand -hex 32)
|
||||
SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse
|
||||
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://localhost:9000?password=
|
||||
|
||||
@@ -78,6 +78,12 @@ msg_info "Configuring SnapOtter"
|
||||
mkdir -p /opt/snapotter_data/files
|
||||
mkdir -p /tmp/snapotter-workspace
|
||||
|
||||
SNAPOTTER_PASS=$(random_password 20)
|
||||
{
|
||||
echo "SnapOtter Credentials"
|
||||
echo "Username: admin"
|
||||
echo "Password: ${SNAPOTTER_PASS}"
|
||||
} >~/snapotter.creds
|
||||
cat <<EOF >/opt/snapotter_data/.env
|
||||
PORT=1349
|
||||
NODE_ENV=production
|
||||
@@ -92,7 +98,7 @@ FEATURE_MANIFEST_PATH=/opt/snapotter/docker/feature-manifest.json
|
||||
U2NET_HOME=/opt/snapotter_data/ai/models/rembg
|
||||
AUTH_ENABLED=true
|
||||
DEFAULT_USERNAME=admin
|
||||
DEFAULT_PASSWORD=admin
|
||||
DEFAULT_PASSWORD=${SNAPOTTER_PASS}
|
||||
LOG_LEVEL=info
|
||||
TRUST_PROXY=true
|
||||
FILE_MAX_AGE_HOURS=72
|
||||
|
||||
@@ -116,10 +116,6 @@ if [[ "$login_mode" == "true" ]]; then
|
||||
# activate Login
|
||||
DISABLE_ADDITIONAL_FEATURES=false
|
||||
SECURITY_ENABLELOGIN=true
|
||||
|
||||
# login credentials
|
||||
SECURITY_INITIALLOGIN_USERNAME=admin
|
||||
SECURITY_INITIALLOGIN_PASSWORD=stirling
|
||||
EOF
|
||||
fi
|
||||
msg_ok "Created Environment Variables"
|
||||
|
||||
@@ -16,14 +16,11 @@ update_os
|
||||
setup_hwaccel
|
||||
|
||||
fetch_and_deploy_gh_release "tunarr" "chrisbenincasa/tunarr" "prebuild" "latest" "/opt/tunarr" "*linux-$(arch_resolve "x64" "arm64").tar.gz"
|
||||
cd /opt/tunarr
|
||||
mv tunarr* tunarr
|
||||
fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-7.1.tar.xz"
|
||||
fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-*.tar.xz"
|
||||
|
||||
msg_info "Set ErsatzTV-ffmpeg links"
|
||||
chmod +x /opt/ErsatzTV-ffmpeg/bin/*
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffmpeg /usr/bin/ffmpeg
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffplay /usr/bin/ffplay
|
||||
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffprobe /usr/bin/ffprobe
|
||||
msg_ok "ffmpeg links set"
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ chmod 775 /var/lib/whisparr-eros/ /opt/Whisparr-Eros/
|
||||
msg_ok "Configured Whisparr-Eros"
|
||||
|
||||
msg_info "Creating Service"
|
||||
WHISPARR_PASS=$(random_password 20)
|
||||
cat <<EOF >/etc/systemd/system/whisparr-eros.service
|
||||
[Unit]
|
||||
Description=Whisparr-Eros Daemon
|
||||
@@ -35,6 +36,7 @@ After=syslog.target network.target
|
||||
[Service]
|
||||
UMask=0002
|
||||
Type=simple
|
||||
Environment=WHISPARR__AUTH__INITIALPASSWORD=${WHISPARR_PASS}
|
||||
ExecStart=/opt/Whisparr-Eros/Whisparr -nobrowser -data=/var/lib/whisparr-eros/
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
@@ -44,6 +46,18 @@ Restart=on-failure
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now whisparr-eros
|
||||
for _ in {1..60}; do
|
||||
grep -qs "created 'whisparr' with the configured password" /var/lib/whisparr-eros/logs/whisparr.txt && break
|
||||
sleep 2
|
||||
done
|
||||
sed -i '/WHISPARR__AUTH__INITIALPASSWORD/d' /etc/systemd/system/whisparr-eros.service
|
||||
systemctl daemon-reload
|
||||
cat <<EOF >~/whisparr-eros.creds
|
||||
Whisparr-Eros Credentials
|
||||
Username: whisparr
|
||||
Password: ${WHISPARR_PASS}
|
||||
EOF
|
||||
chmod 600 ~/whisparr-eros.creds
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
|
||||
@@ -23,7 +23,7 @@ msg_ok "Installed Dependencies"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "tarball"
|
||||
fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "prebuild" "latest" "/opt/yuvomi" "yuvomi-*-dist.tar.gz"
|
||||
|
||||
msg_info "Installing Node.js Dependencies"
|
||||
cd /opt/yuvomi
|
||||
|
||||
@@ -85,12 +85,16 @@ function install() {
|
||||
mkdir -p "${HOME}/.config/code-server/"
|
||||
|
||||
if [ "$preexisting_config" = false ]; then
|
||||
CS_PASS=$(random_password 20)
|
||||
cat <<EOF >"$config_path"
|
||||
bind-addr: 0.0.0.0:8680
|
||||
auth: none
|
||||
password:
|
||||
auth: password
|
||||
password: ${CS_PASS}
|
||||
cert: false
|
||||
EOF
|
||||
chmod 600 "$config_path"
|
||||
echo -e "Coder Code Server\nPassword: ${CS_PASS}" >~/coder-code-server.creds
|
||||
chmod 600 ~/coder-code-server.creds
|
||||
fi
|
||||
systemctl enable -q --now code-server@"$USER"
|
||||
systemctl restart code-server@"$USER"
|
||||
@@ -111,7 +115,7 @@ EOF
|
||||
msg_ok "Created update script (/usr/local/bin/update_coder-code-server)"
|
||||
|
||||
echo -e "${APP} should be reachable by going to the following URL.
|
||||
${BL}http://${IP}:8680${CL} \n"
|
||||
${BL}http://${IP}:8680${CL} (password in ~/coder-code-server.creds)\n"
|
||||
}
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -151,9 +151,9 @@ function install() {
|
||||
else
|
||||
read -rp "${TAB}Set admin username [admin]: " admin_user
|
||||
admin_user=${admin_user:-admin}
|
||||
read -rsp "${TAB}Set admin password [community-scripts.org]: " admin_pass
|
||||
read -rsp "${TAB}Set admin password [random]: " admin_pass
|
||||
echo ""
|
||||
admin_pass=${admin_pass:-community-scripts.org}
|
||||
admin_pass=${admin_pass:-$(random_password 20)}
|
||||
msg_ok "Configured with admin user: ${admin_user}"
|
||||
fi
|
||||
|
||||
@@ -291,8 +291,10 @@ UPDATEEOF
|
||||
msg_ok "Storage: ${BL}${DATA_PATH}${CL}"
|
||||
msg_ok "Config: ${BL}${CONF_PATH}${CL}"
|
||||
if [[ -n "$admin_user" ]]; then
|
||||
echo -e "Copyparty\nUsername: ${admin_user}\nPassword: ${admin_pass}" >~/copyparty.creds
|
||||
chmod 600 ~/copyparty.creds
|
||||
echo ""
|
||||
msg_ok "Login: ${GN}${admin_user}${CL} / ${GN}${admin_pass}${CL}"
|
||||
msg_ok "Login: ${GN}${admin_user}${CL} (password in ~/copyparty.creds)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -189,6 +189,8 @@ chown root:root /usr/local/community-scripts
|
||||
chmod 755 /usr/local/community-scripts
|
||||
msg_ok "Directory prepared"
|
||||
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
msg_warn "FileBrowser runs as root and shows the whole filesystem. Without authentication, anyone who reaches port ${PORT} can read and change every file."
|
||||
echo -n "${TAB}Use No Authentication? (y/N): "
|
||||
read -r noauth_prompt
|
||||
|
||||
@@ -234,9 +236,12 @@ server:
|
||||
- neverWatchPath: "/lost+found"
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: community-scripts.org
|
||||
adminPassword: ${ADMIN_PASS}
|
||||
EOF
|
||||
msg_ok "Configured with default admin (admin / community-scripts.org)"
|
||||
chmod 600 "$CONFIG_PATH"
|
||||
echo -e "FileBrowser Quantum\nUsername: admin\nPassword: ${ADMIN_PASS}" >~/filebrowser-quantum.creds
|
||||
chmod 600 ~/filebrowser-quantum.creds
|
||||
msg_ok "Configured admin (credentials saved in ~/filebrowser-quantum.creds)"
|
||||
fi
|
||||
|
||||
msg_info "Creating service"
|
||||
@@ -276,4 +281,13 @@ EOF
|
||||
fi
|
||||
|
||||
msg_ok "Service created successfully"
|
||||
|
||||
# auth.adminPassword resets the password on every start, so it stays only until the admin exists
|
||||
if grep -q '^\s*adminPassword:' "$CONFIG_PATH"; then
|
||||
for _ in {1..30}; do
|
||||
[[ "$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/health")" == "200" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
sed -i '/^\s*adminPassword:/d' "$CONFIG_PATH"
|
||||
fi
|
||||
msg_ok "${APP} is reachable at: ${BL}http://${IP}:${PORT}${CL}"
|
||||
@@ -122,9 +122,11 @@ if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
chmod 755 /usr/local/community-scripts
|
||||
touch "$DB_PATH"
|
||||
chown root:root "$DB_PATH"
|
||||
chmod 644 "$DB_PATH"
|
||||
chmod 600 "$DB_PATH"
|
||||
msg_ok "Directory created successfully"
|
||||
|
||||
ADMIN_PASS=$(random_password 20)
|
||||
msg_warn "FileBrowser runs as root and shows the whole filesystem. Without authentication, anyone who reaches port ${PORT} can read and change every file."
|
||||
read -r -p "Would you like to use No Authentication? (y/N): " auth_prompt
|
||||
if [[ "${auth_prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
msg_info "Configuring No Authentication"
|
||||
@@ -133,16 +135,18 @@ if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then
|
||||
$STD filebrowser config set -a '0.0.0.0' -p "$PORT" -d "$DB_PATH"
|
||||
$STD filebrowser config set --auth.method=noauth --database "$DB_PATH"
|
||||
if ! filebrowser users update 1 --perm.admin --database "$DB_PATH" &>/dev/null; then
|
||||
$STD filebrowser users add admin community-scripts.org --perm.admin --database "$DB_PATH"
|
||||
$STD filebrowser users add admin "$ADMIN_PASS" --perm.admin --database "$DB_PATH"
|
||||
fi
|
||||
msg_ok "No Authentication configured"
|
||||
else
|
||||
msg_info "Setting up default authentication"
|
||||
msg_info "Setting up authentication"
|
||||
cd /usr/local/community-scripts
|
||||
$STD filebrowser config init -a '0.0.0.0' -p "$PORT" -d "$DB_PATH"
|
||||
$STD filebrowser config set -a '0.0.0.0' -p "$PORT" -d "$DB_PATH"
|
||||
$STD filebrowser users add admin community-scripts.org --perm.admin --database "$DB_PATH"
|
||||
msg_ok "Default authentication configured (admin:community-scripts.org)"
|
||||
$STD filebrowser users add admin "$ADMIN_PASS" --perm.admin --database "$DB_PATH"
|
||||
echo -e "FileBrowser\nUsername: admin\nPassword: ${ADMIN_PASS}" >~/filebrowser.creds
|
||||
chmod 600 ~/filebrowser.creds
|
||||
msg_ok "Authentication configured (credentials saved in ~/filebrowser.creds)"
|
||||
fi
|
||||
|
||||
msg_info "Creating service"
|
||||
@@ -156,7 +160,6 @@ After=network-online.target
|
||||
User=root
|
||||
WorkingDirectory=/usr/local/community-scripts
|
||||
ExecStartPre=/bin/touch /usr/local/community-scripts/filebrowser.db
|
||||
ExecStartPre=/usr/local/bin/filebrowser config set -a "0.0.0.0" -p ${PORT} -d /usr/local/community-scripts/filebrowser.db
|
||||
ExecStart=/usr/local/bin/filebrowser -r / -d /usr/local/community-scripts/filebrowser.db -p ${PORT}
|
||||
Restart=always
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ catch_errors
|
||||
|
||||
# Initialize all core functions (colors, formatting, icons, STD mode)
|
||||
load_functions
|
||||
confirm_not_pve_host
|
||||
require_debian_like
|
||||
|
||||
header_info
|
||||
@@ -49,9 +50,13 @@ msg_ok "Installed Prerequisites"
|
||||
|
||||
msg_info "Installing ${APP}"
|
||||
fetch_and_deploy_gh_release "webmin" "webmin/webmin" "binary" "latest" "/opt/webmin" "webmin_*_all.deb"
|
||||
/usr/share/webmin/changepass.pl /etc/webmin root root &>/dev/null
|
||||
WEBMIN_PASS=$(random_password 20)
|
||||
/usr/share/webmin/changepass.pl /etc/webmin root "$WEBMIN_PASS" &>/dev/null
|
||||
echo -e "Webmin\nUsername: root\nPassword: ${WEBMIN_PASS}" >~/webmin.creds
|
||||
chmod 600 ~/webmin.creds
|
||||
msg_ok "Installed ${APP}"
|
||||
|
||||
get_lxc_ip
|
||||
echo -e "Successfully Installed!! ${APP} should be reachable by going to ${BL}https://${LOCAL_IP}:10000${CL}"
|
||||
echo -e "Login: root, password in ~/webmin.creds"
|
||||
|
||||
@@ -588,11 +588,17 @@ get_vm_ips() {
|
||||
fi
|
||||
if [[ "$agent_enabled" == "1" ]] && command -v qm >/dev/null 2>&1; then
|
||||
debug_log "vm $vmid: querying guest agent"
|
||||
local qm_ips=$(timeout 5 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1" | head -3)
|
||||
local qm_ips=$(timeout 5 qm guest cmd "$vmid" network-get-interfaces 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -v "127.0.0.1")
|
||||
local qm_kept=0
|
||||
for qm_ip in $qm_ips; do
|
||||
if [[ "$qm_ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
debug_log "vm $vmid: found IP $qm_ip via qm guest cmd"
|
||||
ips+="$qm_ip "
|
||||
if ip_in_cidrs "$qm_ip" "${CIDR_LIST[*]}"; then
|
||||
debug_log "vm $vmid: found IP $qm_ip via qm guest cmd"
|
||||
ips+="$qm_ip "
|
||||
((++qm_kept >= 3)) && break
|
||||
else
|
||||
debug_log "vm $vmid: skipping IP $qm_ip via qm guest cmd (outside CIDR_LIST)"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
|
||||
+16
-8
@@ -54,35 +54,35 @@ function run_lxc_clean() {
|
||||
header_info
|
||||
name=$(pct exec "$container" -- hostname)
|
||||
|
||||
pct exec "$container" -- bash -c '
|
||||
pct exec "$container" -- sh -c '
|
||||
BL="\033[36m"; GN="\033[1;92m"; CL="\033[m"
|
||||
name=$(hostname)
|
||||
if [ -e /etc/alpine-release ]; then
|
||||
echo -e "${BL}[Info]${GN} Cleaning $name (Alpine)${CL}\n"
|
||||
printf "%b\n\n" "${BL}[Info]${GN} Cleaning $name (Alpine)${CL}"
|
||||
apk cache clean
|
||||
find /var/log -type f -delete 2>/dev/null
|
||||
find /tmp -mindepth 1 -delete 2>/dev/null
|
||||
apk update
|
||||
elif [ -e /etc/redhat-release ]; then
|
||||
echo -e "${BL}[Info]${GN} Cleaning $name (CentOS)${CL}\n"
|
||||
printf "%b\n\n" "${BL}[Info]${GN} Cleaning $name (CentOS)${CL}"
|
||||
yum clean all
|
||||
find /var/log -type f -delete 2>/dev/null
|
||||
find /tmp -mindepth 1 -delete 2>/dev/null
|
||||
yum update
|
||||
yum upgrade -y
|
||||
else
|
||||
echo -e "${BL}[Info]${GN} Cleaning $name (Debian/Ubuntu)${CL}\n"
|
||||
printf "%b\n\n" "${BL}[Info]${GN} Cleaning $name (Debian/Ubuntu)${CL}"
|
||||
find /var/cache -type f -delete 2>/dev/null
|
||||
find /var/log -type f -delete 2>/dev/null
|
||||
find /tmp -mindepth 1 -delete 2>/dev/null
|
||||
apt -y --purge autoremove
|
||||
apt -y autoclean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apt update
|
||||
fi
|
||||
'
|
||||
}
|
||||
|
||||
failed_containers=()
|
||||
for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
||||
excluded=0
|
||||
for ex in $excluded_containers; do
|
||||
@@ -112,17 +112,25 @@ for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
||||
|
||||
if [ "$template" == "false" ] && [ "$status" == "status: stopped" ]; then
|
||||
echo -e "${BL}[Info]${GN} Starting${BL} $container ${CL} \n"
|
||||
pct start "$container"
|
||||
if ! pct start "$container"; then
|
||||
failed_containers+=("$container (start)")
|
||||
continue
|
||||
fi
|
||||
echo -e "${BL}[Info]${GN} Waiting For${BL} $container${CL}${GN} To Start ${CL} \n"
|
||||
sleep 5
|
||||
run_lxc_clean "$container"
|
||||
run_lxc_clean "$container" || failed_containers+=("$container")
|
||||
echo -e "${BL}[Info]${GN} Shutting down${BL} $container ${CL} \n"
|
||||
pct shutdown "$container" &
|
||||
elif [ "$status" == "status: running" ]; then
|
||||
run_lxc_clean "$container"
|
||||
run_lxc_clean "$container" || failed_containers+=("$container")
|
||||
fi
|
||||
done
|
||||
|
||||
wait
|
||||
header_info
|
||||
if [ "${#failed_containers[@]}" -gt 0 ]; then
|
||||
echo -e "${RD}Cleaning failed in these containers:${CL}"
|
||||
printf '%s\n' "${failed_containers[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GN} Finished, Selected Containers Cleaned. ${CL} \n"
|
||||
@@ -282,7 +282,7 @@ function failure_reason() {
|
||||
function backup_container() {
|
||||
msg_info "Creating backup for container $1"
|
||||
local output
|
||||
if output=$(vzdump $1 --compress zstd --storage $STORAGE_CHOICE -notes-template "{{guestname}} - community-scripts backup updater" 2>&1); then
|
||||
if output=$(_cs_shield_hangup vzdump $1 --compress zstd --storage $STORAGE_CHOICE -notes-template "{{guestname}} - community-scripts backup updater" 2>&1); then
|
||||
msg_ok "Backup created"
|
||||
else
|
||||
log_write "Container $1: vzdump failed:"$'\n'"$output"
|
||||
@@ -651,7 +651,18 @@ for container in $CHOICE; do
|
||||
msg_error "Update failed for container $container ($service, exit code: $exit_code) — attempting restore"
|
||||
log_write "Container $container ($service): FAILED (exit $exit_code) — attempting restore"
|
||||
msg_info "Restoring LXC $container from backup ($STORAGE_CHOICE)"
|
||||
pct stop $container
|
||||
wait
|
||||
pct stop $container &>/dev/null || true
|
||||
for _ in {1..30}; do
|
||||
[[ "$(pct status $container 2>/dev/null)" == "status: stopped" ]] && break
|
||||
sleep 1
|
||||
done
|
||||
if [[ "$(pct status $container 2>/dev/null)" != "status: stopped" ]]; then
|
||||
msg_error "Container $container did not stop, so it cannot be restored"
|
||||
log_result "$container" "$service" "FAILED" "Update failed (exit $exit_code) — container did not stop for the restore"
|
||||
log_write "Container $container ($service): FAILED — container did not stop for the restore"
|
||||
exit 235
|
||||
fi
|
||||
LXC_STORAGE=$(pct config $container | awk -F '[:,]' '/rootfs/ {print $2}')
|
||||
BACKUP_ENTRY=$(pvesm list "$STORAGE_CHOICE" 2>/dev/null | awk -v ctid="$container" '$1 ~ "vzdump-lxc-"ctid"-" || $1 ~ "/ct/"ctid"/" {print $1}' | sort -r | head -n1)
|
||||
if [ -z "$BACKUP_ENTRY" ]; then
|
||||
|
||||
@@ -84,6 +84,7 @@ function update_container() {
|
||||
}
|
||||
|
||||
containers_needing_reboot=()
|
||||
failed_containers=()
|
||||
header_info
|
||||
for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
||||
if [[ " ${excluded_containers[@]} " =~ " $container " ]]; then
|
||||
@@ -101,14 +102,17 @@ for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
||||
template=$(pct config $container | grep -q "template:" && echo "true" || echo "false")
|
||||
if [ "$template" == "false" ] && [ "$status" == "status: stopped" ]; then
|
||||
echo -e "${BL}[Info]${GN} Starting${BL} $container ${CL} \n"
|
||||
pct start $container
|
||||
if ! pct start $container; then
|
||||
failed_containers+=("$container (start)")
|
||||
continue
|
||||
fi
|
||||
echo -e "${BL}[Info]${GN} Waiting For${BL} $container${CL}${GN} To Start ${CL} \n"
|
||||
sleep 5
|
||||
update_container $container
|
||||
update_container $container || failed_containers+=("$container")
|
||||
echo -e "${BL}[Info]${GN} Shutting down${BL} $container ${CL} \n"
|
||||
pct shutdown $container &
|
||||
elif [ "$status" == "status: running" ]; then
|
||||
update_container $container
|
||||
update_container $container || failed_containers+=("$container")
|
||||
fi
|
||||
if [ "$status" == "status: running" ]; then
|
||||
if pct exec "$container" -- [ -e "/var/run/reboot-required" ]; then
|
||||
@@ -119,14 +123,20 @@ for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
||||
# check if patchmon agent is present in container and run a report if found
|
||||
if pct exec "$container" -- [ -e "/usr/local/bin/patchmon-agent" ]; then
|
||||
echo -e "${BL}[Info]${GN} patchmon-agent found in ${BL} $container ${CL}, triggering report. \n"
|
||||
pct exec "$container" -- "/usr/local/bin/patchmon-agent" "report"
|
||||
pct exec "$container" -- "/usr/local/bin/patchmon-agent" "report" || echo -e "${YW}[Warn]${CL} patchmon-agent report failed in $container\n"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
wait
|
||||
header_info
|
||||
echo -e "${GN}The process is complete, and the containers have been successfully updated.${CL}\n"
|
||||
if [ "${#failed_containers[@]}" -gt 0 ]; then
|
||||
echo -e "${RD}The update failed in these containers:${CL}"
|
||||
printf '%s\n' "${failed_containers[@]}"
|
||||
echo ""
|
||||
else
|
||||
echo -e "${GN}The process is complete, and the containers have been successfully updated.${CL}\n"
|
||||
fi
|
||||
if [ "${#containers_needing_reboot[@]}" -gt 0 ]; then
|
||||
echo -e "${RD}The following containers require a reboot:${CL}"
|
||||
for container_name in "${containers_needing_reboot[@]}"; do
|
||||
@@ -134,3 +144,4 @@ if [ "${#containers_needing_reboot[@]}" -gt 0 ]; then
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
[ "${#failed_containers[@]}" -eq 0 ] || exit 1
|
||||
+42
-18
@@ -37,42 +37,37 @@ function truenas_iso_lookup() {
|
||||
|
||||
declare -A latest_stables
|
||||
local pre_releases=()
|
||||
local excluded_codenames="(Angelfish|Bluefin|Cobia|Dragonfish|ElectricEel|Fangtooth)"
|
||||
|
||||
local all_paths=$(
|
||||
curl -fsSL "${_CS_CURL_RETRY[@]}" --max-time 90 "$BASE_URL" |
|
||||
grep -oE 'href="[^"]+\.iso"' |
|
||||
sed 's/href="//; s/"$//' |
|
||||
grep -vE '(MASTER|ALPHA)' |
|
||||
grep -vE '(MASTER|ALPHA|BETA)' |
|
||||
grep -E "$year_pattern"
|
||||
)
|
||||
|
||||
while read -r path; do
|
||||
[[ -n "$path" ]] || continue
|
||||
local filename=$(basename "$path")
|
||||
local version=$(echo "$filename" | sed -E 's/.*TrueNAS-SCALE-([0-9]{2}\.[0-9]{2}(\.[0-9]+)*(-RC[0-9]|-BETA[0-9])?)\.iso.*/\1/')
|
||||
if [[ "$version" =~ (RC|BETA) ]]; then
|
||||
pre_releases+=("$path")
|
||||
[[ "$filename" =~ ^TrueNAS-(SCALE-)?([0-9]+\.[0-9]+(\.[0-9]+)*)\.iso$ ]] || continue
|
||||
local version="${BASH_REMATCH[2]}"
|
||||
local major_version=$(echo "$version" | cut -d'.' -f1,2)
|
||||
local current_stored_path=${latest_stables["$major_version"]:-}
|
||||
|
||||
if [[ -z "$current_stored_path" ]]; then
|
||||
latest_stables["$major_version"]="$path"
|
||||
else
|
||||
local major_version=$(echo "$version" | cut -d'.' -f1,2)
|
||||
local current_stored_path=${latest_stables["$major_version"]:-}
|
||||
if [[ -z "$current_stored_path" ]]; then
|
||||
local stored_version=$(basename "$current_stored_path" | sed -E 's/.*TrueNAS-(SCALE-)?([0-9]+\.[0-9]+(\.[0-9]+)*)\.iso.*/\2/')
|
||||
if printf '%s\n' "$version" "$stored_version" | sort -V | tail -n 1 | grep -qxF "$version"; then
|
||||
latest_stables["$major_version"]="$path"
|
||||
else
|
||||
local stored_version=$(basename "$current_stored_path" | sed -E 's/.*TrueNAS-SCALE-([0-9]{2}\.[0-9]{2}(\.[0-9]+)*)\.iso.*/\1/')
|
||||
if printf '%s\n' "$version" "$stored_version" | sort -V | tail -n 1 | grep -q "$version"; then
|
||||
latest_stables["$major_version"]="$path"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <<<"$all_paths"
|
||||
|
||||
for key in "${!latest_stables[@]}"; do
|
||||
for key in $(printf '%s\n' "${!latest_stables[@]}" | sort -V); do
|
||||
echo "${latest_stables[$key]#/}"
|
||||
done
|
||||
|
||||
for pre in "${pre_releases[@]}"; do
|
||||
echo "${pre#/}"
|
||||
done | sort -V
|
||||
}
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
@@ -115,6 +110,35 @@ function advanced_settings() {
|
||||
vm_prompt_mtu
|
||||
vm_prompt_verbose "no"
|
||||
|
||||
ISOARRAY=()
|
||||
mapfile -t ALL_ISOS < <(truenas_iso_lookup)
|
||||
ISO_COUNT=${#ALL_ISOS[@]}
|
||||
|
||||
if [ $ISO_COUNT -eq 0 ]; then
|
||||
msg_error "Could not find any TrueNAS ISOs."
|
||||
exit 115
|
||||
fi
|
||||
|
||||
for i in "${!ALL_ISOS[@]}"; do
|
||||
ISOPATH="${ALL_ISOS[$i]}"
|
||||
FILENAME=$(basename "$ISOPATH")
|
||||
|
||||
# The newest stable release is preselected
|
||||
if [[ "$i" -eq "$((ISO_COUNT - 1))" ]]; then
|
||||
ISOARRAY+=("$ISOPATH" "$FILENAME" "ON")
|
||||
else
|
||||
ISOARRAY+=("$ISOPATH" "$FILENAME" "OFF")
|
||||
fi
|
||||
done
|
||||
|
||||
if vm_dialog radiolist "SELECT ISO TO INSTALL" --notags --cancel-button Exit-Script \
|
||||
"\nSelect version (Latest stable preselected):" 20 58 12 "${ISOARRAY[@]}"; then
|
||||
SELECTED_ISO="$VM_DIALOG_RESULT"
|
||||
echo -e "${ISO}${BOLD}${DGN}ISO Chosen: ${BGN}$(basename "$SELECTED_ISO")${CL}"
|
||||
else
|
||||
exit_script
|
||||
fi
|
||||
|
||||
if vm_dialog yesno "IMPORT ONBOARD DISKS" --defaultno "Would you like to import onboard disks?" 10 58; then
|
||||
IMPORT_DISKS="yes"
|
||||
else
|
||||
@@ -140,7 +164,7 @@ post_to_api_vm
|
||||
vm_select_storage "$HN"
|
||||
|
||||
if [ -z "${SELECTED_ISO:-}" ]; then
|
||||
SELECTED_ISO=$(truenas_iso_lookup | grep -vE 'RC|BETA' | sort -V | tail -n 1) || true
|
||||
SELECTED_ISO=$(truenas_iso_lookup | tail -n 1) || true
|
||||
|
||||
if [ -z "$SELECTED_ISO" ]; then
|
||||
msg_error "Could not find a stable ISO for fallback."
|
||||
|
||||
Reference in new issue
Block a user