Compare commits

..
Author SHA1 Message Date
github-actions[bot] 3b2ef17f5d chore(ct): sync wireguard defaults from PocketBase 2026-11-03 11:57:42 +00:00
144 changed files with 1063 additions and 4871 deletions

No files matched your search

+2 -18
View File
@@ -58,9 +58,9 @@ body:
Before continuing, verify that the requested project meets the minimum requirements below.
**Projects with fewer than 1,000 stars, or younger than 6 months, are generally not eligible for a script request.**
**Projects with fewer than 1,000 stars are generally not eligible for a script request.**
An automated check verifies the star count and project age for GitHub-hosted projects. If the project doesn't meet these requirements, fill in **Justification for an exception** below — without one, the request is closed automatically. With one, it's flagged for manual review instead. Exceptions are rare and require a clearly verifiable, significant public adoption signal.
Exceptions are only considered where there is a clearly verifiable, significant public adoption signal.
- type: input
id: repository-stars
@@ -134,22 +134,6 @@ body:
- label: I understand that requests outside the project scope may be closed without an extensive explanation.
required: true
- type: textarea
id: justification
attributes:
label: Justification for an exception
description: >-
Only needed if the project has fewer than 1,000 stars or is younger than 6 months (e.g. it has no GitHub
presence, so stars don't apply). Explain the significant public adoption signal. Leave blank if the project
meets the normal requirements — requests that don't meet the requirements and have no justification here are
closed automatically.
placeholder: |
Example:
No public GitHub repository, but the project has 50k+ active installs reported by its own telemetry
dashboard and is covered by several established self-hosting publications.
validations:
required: false
- type: markdown
attributes:
value: |
-187
View File
@@ -1,187 +0,0 @@
## 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))
+10 -10
View File
@@ -17,6 +17,16 @@ Fixes #
**Tested on:** <!-- e.g. PVE 9.2 / Debian 13 / fresh install + update. Write "not tested" if you could not run it. -->
---
## 🤖 AI Assistance (**X** in brackets)
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
> Select exactly one option.
- [ ] **No AI used** – Scripts were written without AI assistance.
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/DevScripts/blob/main/.github/agents/pve-script-creator.agent.md) as guidance, and the output has been reviewed and corrected to match those guidelines.
---
## 🛠️ Type of Change (**X** in brackets)
@@ -60,13 +70,3 @@ Guidance:
-->
<!-- The advisory block is only active once it is OUTSIDE this comment. -->
---
## 🤖 AI Assistance (**X** in brackets)
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
> Select exactly one option.
- [ ] **No AI used** – Scripts were written without AI assistance.
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/DevScripts/blob/main/.github/agents/pve-script-creator.agent.md) as guidance, and the output has been reviewed and corrected to match those guidelines.
-114
View File
@@ -1,114 +0,0 @@
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
-27
View File
@@ -1,27 +0,0 @@
name: Check Script Repo URL
on:
pull_request:
branches: ["main"]
paths:
- "ct/**"
- "install/**"
- "vm/**"
- "tools/**"
permissions:
contents: read
jobs:
check:
if: github.repository == 'community-scripts/ProxmoxVE'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Reject DevScripts URLs
run: |
if grep -rnE 'community-scripts/(ProxmoxVED|DevScripts)/' ct install vm tools; then
echo "::error::These lines point at DevScripts, so telemetry files the runs there. Use community-scripts/ProxmoxVE."
exit 1
fi
+40 -44
View File
@@ -1,4 +1,4 @@
name: Flag PRs Missing Template
name: Close PRs Missing Template
on:
pull_request_target:
@@ -14,12 +14,10 @@ jobs:
issues: write
contents: read
steps:
- name: Check PR against the template, comment and label instead of closing
- name: Close PR if it does not follow the PR template
uses: actions/github-script@v9
with:
script: |
const MARKER = "<!-- pr-template-check -->";
const LABELS = ["missing pr template", "stale"];
const pr = context.payload.pull_request;
const prNumber = pr.number;
const author = pr.user.login;
@@ -46,11 +44,11 @@ jobs:
return;
}
const prLabels = pr.labels.map((label) => label.name);
const labels = pr.labels.map((label) => label.name);
const skipLabels = ["automated pr", "keep-open"];
if (skipLabels.some((label) => prLabels.includes(label))) {
core.info(`PR #${prNumber} has a skip label (${prLabels.join(", ")}) — skipping template validation.`);
if (skipLabels.some((label) => labels.includes(label))) {
core.info(`PR #${prNumber} has a skip label (${labels.join(", ")}) — skipping template validation.`);
return;
}
@@ -116,52 +114,50 @@ jobs:
failures.push("At least one **Type of Change** checkbox must be checked.");
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber, per_page: 100
});
const mine = comments.find(c => c.body && c.body.includes(MARKER));
if (failures.length === 0) {
core.info(`PR #${prNumber} follows the PR template.`);
if (mine && !mine.body.startsWith(MARKER + "\n✅")) {
await github.rest.issues.updateComment({
owner, repo, comment_id: mine.id,
body: MARKER + "\n✅ **Template check passed.** Thanks for fixing the description — the `stale` mark has been removed.",
});
}
for (const name of LABELS) {
if (prLabels.includes(name)) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
}
}
return;
}
core.info(`PR #${prNumber} — template validation failed, flagging instead of closing.`);
core.info(`Closing PR #${prNumber} — template validation failed.`);
const templateUrl =
"https://github.com/community-scripts/ProxmoxVE/blob/main/.github/pull_request_template.md";
const failureList = failures.map((item) => `- ${item}`).join("\n");
const text =
MARKER + "\n" +
`👋 Hi @${author},\n\n` +
`This pull request does not follow the [PR template](${templateUrl}) yet:\n\n` +
failureList + "\n\n" +
`**Edit the PR description** to fix it — no new PR needed. This check runs again on every edit and clears itself once everything is in place.\n\n` +
`Until then the PR is marked \`stale\` and will be closed in **7 days**.\n\n` +
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.\n\n` +
`Thank you for contributing! 🙏`;
const comment = [
`👋 Hi @${author},`,
``,
`This pull request was closed because it does not follow the [PR template](${templateUrl}).`,
``,
`Please fix the following and open a new PR (or reopen this one after updating the description):`,
``,
failureList,
``,
`> Use the template sections, fill in the description, check all prerequisite boxes, and select at least one type of change.`,
``,
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.`,
``,
`Thank you for contributing! 🙏`,
].join("\n");
if (mine) {
if (mine.body !== text) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body: text });
}
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: text });
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: comment,
});
const toAdd = LABELS.filter(name => !prLabels.includes(name));
if (toAdd.length > 0) {
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: toAdd });
}
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
state: "closed",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ["missing pr template"],
});
+6 -13
View File
@@ -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&expand=type', {
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
@@ -239,21 +239,14 @@ 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(patch)
body: JSON.stringify({
name: record.name || record.slug,
last_update_commit: prUrl,
is_dev: false
})
});
if (!patchRes.ok) {
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
-780
View File
@@ -1,780 +0,0 @@
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 }}
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 = [];
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 : '', name: `${os ? `${name}-${os}` : name}-${platform}` });
}
}
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].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(' '));
// 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].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: 90 # install (45) + update (30) + 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' || '' }}
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,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 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);
} 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'}`) || {
script: m.script, os: m.os || 'default', platform: m.platform || 'pve',
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", 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;
return r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
};
// 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));
}
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>`, ''] : []),
...(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}`);
}
-141
View File
@@ -1,141 +0,0 @@
name: Request Script Eligibility Check
# category.slug is the same string GitHub uses in the "new discussion" URL
# (?category=request-script, linked from every repo's issue chooser), so this
# guard matches the live category without needing its numeric id.
on:
discussion:
types: [created, edited, reopened]
jobs:
check-eligibility:
if: github.repository == 'community-scripts/ProxmoxVE' && github.event.discussion.category.slug == 'request-script'
runs-on: ubuntu-latest
permissions:
discussions: write
issues: write
contents: read
steps:
- name: Verify stars/age, flag or close
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const discussion = context.payload.discussion;
if (!discussion || discussion.state === "closed") {
core.info("Discussion missing or already closed — skipping.");
return;
}
const body = discussion.body || "";
const lines = body.split("\n");
const fields = {};
let current = null;
for (const line of lines) {
const m = line.match(/^###\s+(.+?)\s*$/);
if (m) {
current = m[1].trim();
fields[current] = [];
} else if (current) {
fields[current].push(line);
}
}
function fieldText(label) {
const raw = (fields[label] || []).join("\n").trim();
return raw === "_No response_" ? "" : raw;
}
const sourceRepo = fieldText("Source Repository");
const justification = fieldText("Justification for an exception");
let eligible = false;
let verified = false;
const ghMatch = sourceRepo.match(/^https?:\/\/(?:www\.)?github\.com\/([^\/\s]+)\/([^\/\s#?]+?)(?:\.git)?\/?(?:[#?].*)?$/i);
if (ghMatch) {
const [, repoOwner, repoName] = ghMatch;
try {
const { data: repoData } = await github.rest.repos.get({ owner: repoOwner, repo: repoName });
const stars = repoData.stargazers_count;
const createdAt = new Date(repoData.created_at);
const now = new Date();
const ageMonths =
(now.getFullYear() - createdAt.getFullYear()) * 12 +
(now.getMonth() - createdAt.getMonth()) -
(now.getDate() < createdAt.getDate() ? 1 : 0);
eligible = stars >= 1000 && ageMonths >= 6;
verified = true;
core.info(`Verified ${repoOwner}/${repoName}: ${stars} stars, ${ageMonths} months old, eligible=${eligible}`);
} catch (err) {
core.warning(`Could not verify ${repoOwner}/${repoName}: ${err.message} — treating as unverifiable.`);
}
} else {
core.info("Source Repository is not a github.com URL — cannot auto-verify, treating as unverifiable.");
}
if (eligible) {
core.info(`Discussion #${discussion.number} meets the requirements — no action.`);
return;
}
const labels = (discussion.labels || []).map(l => l.name);
const reasonLine = verified
? "the source repository does not meet the 1,000-star / 6-month minimum adoption requirement"
: "the source repository's star count and age could not be verified automatically";
if (justification) {
if (labels.includes("investigation")) {
core.info(`Discussion #${discussion.number} already flagged for investigation — skipping.`);
return;
}
try {
await github.rest.issues.createLabel({
owner, repo, name: "investigation",
color: "fbca04",
description: "Below the normal script-request threshold; a justification was given and needs maintainer review.",
});
} catch (err) {
if (err.status !== 422) throw err;
}
const labelQuery = await github.graphql(
`query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { label(name: "investigation") { id } } }`,
{ owner, repo }
);
const labelNodeId = labelQuery.repository.label.id;
await github.graphql(
`mutation($labelableId: ID!, $labelIds: [ID!]!) { addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) { clientMutationId } }`,
{ labelableId: discussion.node_id, labelIds: [labelNodeId] }
);
await github.graphql(
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
{
discussionId: discussion.node_id,
body:
`Thanks for the justification — ${reasonLine}, so this has been flagged \`investigation\` for a maintainer ` +
`to review manually rather than closed automatically.`,
}
);
} else {
await github.graphql(
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
{
discussionId: discussion.node_id,
body:
`This request doesn't meet the minimum requirements (1,000 stars, project at least 6 months old) — ` +
`${reasonLine}, and no justification was given. Closing automatically.\n\n` +
`If you believe this project qualifies for an exception, edit this discussion to fill in ` +
`**Justification for an exception** and reopen it — it will then be flagged for manual review instead of closed again.`,
}
);
await github.graphql(
`mutation($discussionId: ID!) { closeDiscussion(input: { discussionId: $discussionId, reason: OUTDATED }) { discussion { id } } }`,
{ discussionId: discussion.node_id }
);
}
-192
View File
@@ -1,192 +0,0 @@
#!/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"
-193
View File
@@ -1,193 +0,0 @@
#!/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
#
# 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
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"
-220
View File
@@ -1,220 +0,0 @@
#!/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
#
# 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
# 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"
-643
View File
@@ -1,643 +0,0 @@
#!/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; }
[[ "$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}"
kind="${SCRIPT%%/*}" # ct or vm
app=$(basename "$SCRIPT" .sh)
job="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${PLATFORM}-${kind}-${app}-${OS:-default}"
tree="$SERVE_ROOT/$job"
base="$SERVE_URL/$job"
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=""
# 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}"
}
finish() {
[[ -f "$RAW_LOG" ]] && strip_ansi <"$RAW_LOG" >"$LOG"
[[ -f "$RAW_LOG" ]] && collect_install_log
[[ -f "$UPDATE_RAW" ]] && strip_ansi <"$UPDATE_RAW" >"$UPDATE_LOG"
rm -f "$UPDATE_RAW"
jq -n \
--arg script "$SCRIPT" --arg kind "$kind" --arg platform "$PLATFORM" --arg os "${OS:-default}" --arg status "$status" \
--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,
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"
}
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"
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"
if [[ "$kind" == "vm" ]]; then
vm_build_and_check
exit 0
fi
# --- Build ----------------------------------------------------------------------
echo "Building ${SCRIPT} (${OS:-default OS}) on ${PLATFORM} as CT ${ctid} from ${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="$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="$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="[^"]+"' "$tree/$SCRIPT" | cut -d'"' -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]')
for f in "$tree/install/${app}-install.sh" "$tree/install/${nsapp}-install.sh" "$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")"' _ "$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.
pattern=$(grep -oE 'https?://\$\{IP\}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?' "$tree/$SCRIPT" | head -n1)
[[ -n "$pattern" && -n "$ct_ip" ]] && url="${pattern/\$\{IP\}/$ct_ip}"
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. `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_+-]+)+$" "$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}"
: >"$UPDATE_RAW"
update_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
echo "──────── update test (live) ────────"
echo "::stop-commands::${update_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 "$UPDATE_RAW" | live_filter) 2>&1 &
update_pid=$!
wait "$update_pid"
update_rc=$?
update_pid=""
sleep 1
echo "::${update_token}::"
update_failure_line() {
strip_ansi <"$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
check_health screenshot-update.png || update_fail "after the update: $health_problem"
status="pass"
finish
-140
View File
@@ -1,140 +0,0 @@
# 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
}
+73 -95
View File
@@ -22,112 +22,90 @@ jobs:
const now = new Date();
const owner = context.repo.owner;
const repo = context.repo.repo;
const TEMPLATE_LABEL = "missing pr template";
const MAINTAINERS = ["OWNER", "MEMBER", "COLLABORATOR"];
const days = d => (now - d) / (1000 * 60 * 60 * 24);
const staleNotice = author =>
`@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`;
// Only fires for a maintainer's label: labels set with GITHUB_TOKEN raise no event.
// --- When stale label is added, comment immediately ---
if (context.eventName === "pull_request_target" && context.payload.action === "labeled") {
const pr = context.payload.pull_request;
const labels = (pr.labels || []).map(l => l.name);
if (context.payload.label?.name === "stale" && !labels.includes(TEMPLATE_LABEL)) {
const label = context.payload.label?.name;
if (label === "stale") {
const author = context.payload.pull_request.user.login;
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number, body: staleNotice(pr.user.login)
owner,
repo,
issue_number: context.payload.pull_request.number,
body: `@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`
});
}
return;
}
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
// --- Scheduled run: check all stale PRs ---
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: "open",
per_page: 100
});
for (const pr of prs) {
const labels = pr.labels.map(l => l.name);
const author = pr.user.login;
const hasStale = labels.includes("stale");
if (labels.includes("keep-open")) {
if (hasStale) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
}
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100
const hasStale = pr.labels.some(l => l.name === "stale");
if (!hasStale) continue;
// Get timeline events to find when stale label was added
const { data: events } = await github.rest.issues.listEvents({
owner,
repo,
issue_number: pr.number,
per_page: 100
});
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner, repo, pull_number: pr.number, per_page: 100
});
const lastCommitDate = commits.length > 0
? new Date(commits[commits.length - 1].commit.author.date)
: new Date(pr.created_at);
const humanComments = comments.filter(c => c.user?.type !== "Bot");
// -------------------------------------------------------
// Auto-label PRs with no activity in the last 14 days,
// but ONLY if someone other than the author has engaged.
// -------------------------------------------------------
if (!hasStale) {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner, repo, pull_number: pr.number, per_page: 100
});
const engaged =
reviews.some(r => r.user?.type !== "Bot" && r.user?.login !== author) ||
humanComments.some(c => c.user?.login !== author);
if (!engaged) continue;
const lastCommentDate = humanComments.length > 0
? new Date(humanComments[humanComments.length - 1].created_at)
: null;
const lastActivityDate = lastCommentDate && lastCommentDate > lastCommitDate
? lastCommentDate
: lastCommitDate;
if (days(lastActivityDate) > 14) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ["stale"] });
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: staleNotice(author) });
}
continue;
}
// -------------------------------------------------------
// Manage already-stale PRs
// -------------------------------------------------------
const events = await github.paginate(github.rest.issues.listEvents, {
owner, repo, issue_number: pr.number, per_page: 100
});
const staleDates = events
// Find the most recent time the stale label was added
const staleLabelEvents = events
.filter(e => e.event === "labeled" && e.label?.name === "stale")
.map(e => new Date(e.created_at));
if (staleDates.length === 0) continue;
const staleLabelDate = new Date(Math.max(...staleDates));
const isTemplateIssue = labels.includes(TEMPLATE_LABEL);
// A push ends an inactivity mark; a template mark only ends via close-invalid-pr-template.yml's re-check.
if (!isTemplateIssue && lastCommitDate > staleLabelDate) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (staleLabelEvents.length === 0) continue;
const staleLabelDate = new Date(staleLabelEvents[0].created_at);
const daysSinceStale = (now - staleLabelDate) / (1000 * 60 * 60 * 24);
// Check for new commits since stale label was added
const { data: commits } = await github.rest.pulls.listCommits({
owner,
repo,
pull_number: pr.number
});
const lastCommitDate = new Date(commits[commits.length - 1].commit.author.date);
const author = pr.user.login;
// If there are new commits after the stale label, remove it
if (lastCommitDate > staleLabelDate) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr.number,
name: "stale"
});
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
owner,
repo,
issue_number: pr.number,
body: `@${author} Recent activity detected. Removing stale label.`
});
continue;
}
// A maintainer comment after the mark restarts the 7 days.
const maintainerDates = humanComments
.filter(c => MAINTAINERS.includes(c.author_association) && c.user?.login !== author)
.map(c => new Date(c.created_at))
.filter(d => d > staleLabelDate);
const clockStart = maintainerDates.length > 0 ? new Date(Math.max(...maintainerDates)) : staleLabelDate;
if (days(clockStart) <= 7) continue;
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: isTemplateIssue
? `@${author} Closing: the PR template requirements listed above were still missing 7 days later. Once they are in place, please open a new pull request.`
: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
});
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: "closed" });
// If 7 days have passed since stale label, close the PR
else if (daysSinceStale > 7) {
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: "closed"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
});
}
}
+186 -268
View File
@@ -125,9 +125,6 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
@@ -140,13 +137,6 @@ 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>
@@ -581,248 +571,14 @@ 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))
## 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
- #### 🐞 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))
@@ -839,29 +595,6 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- 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
@@ -1475,4 +1208,189 @@ 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))
- 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))
+3 -7
View File
@@ -55,13 +55,9 @@ update_deb_based() {
setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "binary"
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy updated; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Updating xCaddy"
$STD xcaddy build
msg_ok "Updated xCaddy"
fi
msg_info "Updating xCaddy"
$STD xcaddy build
msg_ok "Updated xCaddy"
fi
fi
msg_ok "Updated successfully!"
-57
View File
@@ -1,57 +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://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}"
+2 -2
View File
@@ -36,7 +36,7 @@ function update_script() {
systemctl stop cinephage
msg_ok "Stopped Cinephage"
NODE_VERSION="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
msg_info "Building Cinephage"
@@ -48,7 +48,7 @@ function update_script() {
msg_ok "Built Cinephage"
msg_info "Updating Camoufox"
$STD /opt/cinephage/node_modules/.bin/camoufox fetch
$STD /opt/cinephage/node_modules/.bin/camoufox-js fetch
msg_ok "Updated Camoufox"
msg_info "Starting Cinephage"
+73
View File
@@ -0,0 +1,73 @@
#!/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
View File
@@ -26,7 +26,7 @@ function update_script() {
check_container_storage
check_container_resources
NODE_VERSION="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
ensure_dependencies build-essential
if command -v cross-seed &>/dev/null; then
+38 -10
View File
@@ -38,29 +38,57 @@ function update_script() {
$STD systemctl stop databasus
msg_ok "Stopped Databasus"
ensure_dependencies libedit2 libgssapi-krb5-2 libncurses6 libreadline8t64
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"
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 env VITE_APP_VERSION="${RELEASE}" pnpm run build
$STD pnpm run build
cd /opt/databasus/backend
$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
$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
mkdir -p /opt/databasus/ui/build
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
sed -i '/^APP_VERSION=/d' /.env
echo "APP_VERSION=${RELEASE}" >>/.env
cp -r /opt/databasus/backend/migrations /opt/databasus/
msg_ok "Updated Databasus"
restore_backup
chown -R postgres:postgres /opt/databasus
if ! grep -q "EnvironmentFile=/.env" /etc/systemd/system/databasus.service; then
-11
View File
@@ -58,17 +58,6 @@ function update_script() {
$STD git pull origin main
msg_ok "Updated Discourse"
if check_for_gh_release "libvips" "libvips/libvips"; then
ensure_dependencies meson ninja-build pkg-config libglib2.0-dev libexpat1-dev libjpeg-dev libpng-dev libwebp-dev libtiff-dev libheif-dev libjxl-dev librsvg2-dev libcgif-dev libexif-dev liblcms2-dev liborc-0.4-dev libimagequant-dev libpango1.0-dev
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "libvips" "libvips/libvips" "prebuild" "latest" "/opt/libvips" "vips-*.tar.xz"
msg_info "Building libvips"
cd /opt/libvips
$STD meson setup build --prefix=/usr/local --libdir=lib --buildtype=release -Dmodules=disabled -Dmagick=disabled -Dexamples=false
$STD ninja -C build install
ldconfig
msg_ok "Built libvips"
fi
msg_info "Updating Dependencies"
RUBY_TARGET=$(grep -A1 '^RUBY VERSION' /opt/discourse/Gemfile.lock | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
RUBY_VERSION="${RUBY_TARGET:-4.0.7}" RUBY_INSTALL_RAILS="false" setup_ruby
+2 -2
View File
@@ -11,8 +11,8 @@ var_tags="${var_tags:-web}"
var_cpu="${var_cpu:-1}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-8}"
var_os="${var_os:-Debian}"
var_version="${var_version:-13}"
var_os="${var_os:-ubuntu}"
var_version="${var_version:-24.04}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-0}"
-74
View File
@@ -1,74 +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: 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}"
-66
View File
@@ -1,66 +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://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}"
+6 -23
View File
@@ -45,32 +45,15 @@ 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/v9/cmd.Version=$(cat ~/.gotenberg)'" \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
cmd/gotenberg/main.go
msg_ok "Built gotenberg"
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 "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"
msg_info "Starting Service"
systemctl start gotenberg
+27 -2
View File
@@ -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 --break-system-packages
pip install -U runlike
echo "Updating ${container} image ${CONTAINER_IMAGE}"
DOCKER_COMMAND="$(runlike --use-volume-id "${container}")"
docker rm --force "${container}"
@@ -70,7 +70,32 @@ function update_script() {
exit
fi
if [ "$UPD" == "4" ]; then
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
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"
exit
fi
}
+20 -44
View File
@@ -75,7 +75,7 @@ EOF
BASE_DIR=${STAGING_DIR}/base-images
SOURCE_DIR=${STAGING_DIR}/image-source
cd /tmp
RELEASE="v3.4.1"
RELEASE="v3.3.5"
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 pigz
ensure_dependencies liblcms2-dev libjpeg62-turbo-dev libspng-dev libexif-dev libopenjp2-7-dev
$STD git pull
for library in "${libraries[@]}"; do
compile_"$library"
@@ -131,18 +131,7 @@ 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="${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"
PREV_IMMICH="$(cat ~/.immich)"
# 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"
@@ -152,7 +141,6 @@ 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
@@ -161,13 +149,13 @@ EOF
PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -1)
PG_VERSION=${PG_VERSION:-16}
[[ -f ~/.vchord_version ]] && mv ~/.vchord_version ~/.vectorchord
if check_for_gh_release "VectorChord" "supervc-stack/VectorChord" "${VCHORD_RELEASE}" "updated together with Immich after testing"; then
if check_for_gh_release "VectorChord" "tensorchord/VectorChord" "${VCHORD_RELEASE}" "updated together with Immich after testing"; then
# dead tuples in smart_search/face_search make the REINDEX below fail with
# "missing chunk ... for toast value" on VectorChord 1.0.0 (#15588); must vacuum
# while still on the old extension version, a post-upgrade vacuum errors instead
$STD sudo -u postgres psql -d immich -c "VACUUM (ANALYZE) smart_search;"
$STD sudo -u postgres psql -d immich -c "VACUUM (ANALYZE) face_search;"
fetch_and_deploy_gh_release "VectorChord" "supervc-stack/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${PG_VERSION}-vchord_*_$(arch_resolve).deb"
fetch_and_deploy_gh_release "VectorChord" "tensorchord/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${PG_VERSION}-vchord_*_$(arch_resolve).deb"
systemctl restart postgresql
$STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vector UPDATE;"
$STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vchord UPDATE;"
@@ -176,6 +164,14 @@ 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"
@@ -197,6 +193,8 @@ 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
@@ -234,7 +232,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,restic,npm:@openapitools/openapi-generator-cli
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
mise_ok=0
for i in 1 2 3; do
@@ -251,8 +249,7 @@ 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)"
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"
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"
gunzip -f /tmp/extism-js.gz
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
rm -f /tmp/extism-js
@@ -277,27 +274,6 @@ 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)
@@ -316,7 +292,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -334,7 +310,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -443,7 +419,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 -- third_party
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
mkdir build
cd build
$STD cmake \
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -36,7 +36,7 @@ function update_script() {
source ~/.bashrc
if ! command -v deno &>/dev/null; then
export DENO_INSTALL="/usr/local"
curl -fsSL https://deno.land/install.sh | CI=1 $STD sh -s
curl -fsSL https://deno.land/install.sh | $STD sh -s -- -y
else
$STD deno upgrade
fi
+2 -2
View File
@@ -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:-2048}"
var_ram="${var_ram:-1024}"
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="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
if check_for_gh_release "myip" "jason5ng32/MyIP"; then
msg_info "Stopping Services"
+1 -4
View File
@@ -46,10 +46,7 @@ EOF
systemctl daemon-reload
fi
# n8n-nodes-base 2.46.0 pins both @napi-rs/canvas musl binaries as hard
# dependencies, which no os/cpu/libc combination can satisfy. Drop --force
# once upstream makes them optional again.
$STD npm install -g n8n@latest --force
$STD npm install -g n8n@latest
systemctl restart n8n
msg_ok "Updated n8n"
msg_ok "Updated successfully!"
-3
View File
@@ -31,8 +31,6 @@ function update_script() {
exit
fi
echo "x /tmp/nexterm-sea-*" >/etc/tmpfiles.d/nexterm.conf
if check_for_gh_release "nexterm-engine" "gnmyt/Nexterm"; then
msg_info "Stopping nexterm-engine"
systemctl stop nexterm-engine
@@ -48,7 +46,6 @@ function update_script() {
if check_for_gh_release "nexterm-server" "gnmyt/Nexterm"; then
msg_info "Stopping nexterm-server"
systemctl stop nexterm-server
rm -rf /tmp/nexterm-sea-*
msg_ok "Stopped nexterm-server"
fetch_and_deploy_gh_release "nexterm-server" "gnmyt/Nexterm" "singlefile" "latest" "/opt/nexterm/server" "nexterm-server-linux-$(arch_resolve "x64" "arm64")"
+1 -6
View File
@@ -32,14 +32,9 @@ 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 --no-same-owner
echo "${RELEASE}" >~/.openobserve
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
systemctl start openobserve
msg_ok "Updated OpenObserve"
msg_ok "Updated successfully!"
+25 -41
View File
@@ -32,52 +32,36 @@ function update_script() {
if check_for_gh_release "paperless-ai" "clusterzx/paperless-ai"; then
msg_info "Stopping Service"
systemctl stop paperless-ai
if [[ -f /etc/systemd/system/paperless-rag.service ]]; then
systemctl stop paperless-rag
fi
systemctl stop paperless-ai paperless-rag
msg_ok "Stopped Service"
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 "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"
msg_info "Updating Paperless-AI"
cd /opt/paperless-ai
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
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
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!"
@@ -89,7 +73,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 "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
-79
View File
@@ -1,79 +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://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"
-9
View File
@@ -30,15 +30,6 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
if ! grep -q "^Environment=HOME=" /etc/systemd/system/planka.service; then
msg_info "Updating Service"
sed -i -e '/^Environment=NODE_ENV=/d' -e 's|^ExecStart=.*|Environment=NODE_ENV=production\nEnvironment=HOME=/root\nExecStart=/usr/bin/node app.js --prod|' /etc/systemd/system/planka.service
systemctl daemon-reload
systemctl restart planka
msg_ok "Updated Service"
fi
if check_for_gh_release "planka" "plankanban/planka"; then
msg_info "Stopping Service"
systemctl stop planka
+6 -7
View File
@@ -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/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
APP="Pocketbase"
var_tags="${var_tags:-database}"
@@ -29,21 +29,20 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
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
if check_for_gh_release "pocketbase" "pocketbase/pocketbase"; then
msg_info "Stopping Service"
systemctl stop pocketbase
msg_ok "Stopped Service"
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 "Updating ${APP}"
/opt/pocketbase/pocketbase update
echo "${CHECK_UPDATE_RELEASE}" >~/.pocketbase
msg_ok "Updated ${APP}"
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
}
+27 -1
View File
@@ -66,7 +66,33 @@ function update_script() {
exit
fi
if [ "$UPD" == "3" ]; then
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
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"
exit
fi
if [ "$UPD" == "4" ]; then
+9 -21
View File
@@ -107,9 +107,8 @@ 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 ||
! grep -q "coep_header" /etc/angie/http.d/romm.conf; then
msg_info "Updating Angie config (decode/cache locations, player isolation headers)"
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"
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"
@@ -148,20 +147,6 @@ 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 _;
@@ -170,9 +155,6 @@ 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 {
@@ -182,6 +164,12 @@ 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;
@@ -221,7 +209,7 @@ server {
}
}
EOF
msg_ok "Updated Angie config"
msg_ok "Added /decode and /cache locations"
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
-10
View File
@@ -32,16 +32,6 @@ function update_script() {
exit
fi
ensure_dependencies redis-server
systemctl enable -q --now redis-server
if ! grep -q "BACKEND_CACHE_REDIS_URI" /opt/teable/.env; then
sed -i '/^BACKEND_CACHE_SQLITE_URI=/d' /opt/teable/.env
sed -i 's|^BACKEND_CACHE_PROVIDER=.*|BACKEND_CACHE_PROVIDER=redis\nBACKEND_CACHE_REDIS_URI=redis://127.0.0.1:6379/0|' /opt/teable/.env
sed -i 's|^After=network.target postgresql.service$|After=network.target postgresql.service redis-server.service|' /etc/systemd/system/teable.service
systemctl daemon-reload
systemctl restart teable
fi
if check_for_gh_release "teable" "teableio/teable"; then
msg_info "Stopping Service"
systemctl stop teable
+1 -1
View File
@@ -47,7 +47,7 @@ function update_script() {
$STD apt install -y aspnetcore-runtime-10.0
fi
RELEASE=$(get_latest_github_release "TechnitiumSoftware/DnsServer")
RELEASE=$(curl -fsSL https://technitium.com/dns/ | grep -oP 'Version \K[\d.]+')
if [[ ! -f ~/.technitium || ${RELEASE} != "$(cat ~/.technitium 2>/dev/null)" ]]; then
fetch_and_deploy_from_url "https://download.technitium.com/dns/DnsServerPortable.tar.gz" /opt/technitium/dns
echo "${RELEASE}" >~/.technitium
+1 -19
View File
@@ -31,30 +31,12 @@ function update_script() {
fi
JAVA_VERSION="25" setup_java
if ! grep -q "install.data_dir" /etc/thingsboard/conf/thingsboard.conf; then
msg_info "Restoring Package Defaults"
cat <<'EOF' >>/etc/thingsboard/conf/thingsboard.conf
export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export LOG_FILENAME=thingsboard.out
export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions
export SQL_DATA_FOLDER=/usr/share/thingsboard/data/sql
EOF
systemctl restart thingsboard
msg_ok "Restored Package Defaults"
fi
if check_for_gh_release "thingsboard" "thingsboard/thingsboard"; then
msg_info "Stopping Service"
systemctl stop thingsboard
msg_ok "Stopped Service"
DPKG_FORCE_CONFOLD=1 fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
msg_info "Running Database Upgrade"
$STD /usr/share/thingsboard/bin/install/upgrade.sh
+2 -14
View File
@@ -32,14 +32,7 @@ function update_script() {
NODE_VERSION="22" setup_nodejs
RELEASE=""
if ! grep -qs '^DATABASE_URL=' /opt/tududi/backend/.env; then
RELEASE="v1.15.0"
msg_warn "Tududi 2.0 runs on PostgreSQL only, so this SQLite install stays on 1.15.0"
msg_warn "To move to 2.0, export in Settings > Backups and import into a new Tududi LXC"
fi
if check_for_gh_release "tududi" "chrisvel/tududi" "${RELEASE}" "2.0 needs PostgreSQL"; then
if check_for_gh_release "tududi" "chrisvel/tududi"; then
msg_info "Stopping Service"
systemctl stop tududi
msg_ok "Stopped Service"
@@ -50,14 +43,9 @@ function update_script() {
else
cp /opt/tududi/.env /opt/tududi.env
fi
if ! grep -q '^TUDUDI_UPLOAD_PATH=' /opt/tududi.env; then
mkdir -p /opt/tududi-uploads
[[ -d /opt/tududi/backend/uploads ]] && cp -a /opt/tududi/backend/uploads/. /opt/tududi-uploads/
echo "TUDUDI_UPLOAD_PATH=/opt/tududi-uploads" >>/opt/tududi.env
fi
msg_ok "Backed up env file"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tududi" "chrisvel/tududi" "tarball" "${CHECK_UPDATE_RELEASE}" "/opt/tududi"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tududi" "chrisvel/tududi" "tarball" "latest" "/opt/tududi"
msg_info "Updating Tududi"
cd /opt/tududi
+6 -3
View File
@@ -37,6 +37,8 @@ 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"
@@ -50,12 +52,13 @@ 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-*.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-7.1.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/ffprobe /usr/bin/ffprobe
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
msg_ok "ffmpeg links set"
msg_info "Starting Service"
-1
View File
@@ -56,4 +56,3 @@ 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}"
-35
View File
@@ -30,41 +30,6 @@ function update_script() {
exit
fi
if [[ -d /opt/wikijs/backend ]]; then
NODE_VERSION="26" setup_nodejs
if check_for_gh_release "wikijs" "requarks/wiki"; then
PUPPETEER_INSTALLED=$([[ -d /opt/wikijs/backend/node_modules/puppeteer ]] && echo "true" || echo "false")
msg_info "Stopping Service"
systemctl stop wikijs
msg_ok "Stopped Service"
create_backup /opt/wikijs/config.yml
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "wikijs" "requarks/wiki" "prebuild" "latest" "/opt/wikijs" "wiki-js.tar.gz"
restore_backup
cd /opt/wikijs/backend
if [[ "$(arch_resolve)" == "arm64" ]]; then
msg_info "Building Wiki.js Dependencies"
$STD npm ci --omit=dev
msg_ok "Built Wiki.js Dependencies"
fi
if [[ "$PUPPETEER_INSTALLED" == "true" ]]; then
msg_info "Reinstalling Puppeteer"
$STD npm install --no-save --force --include=optional --no-ignore-scripts --no-audit --no-fund "puppeteer@$(sed -n 's/^installVersion: *//p' modules/extensions/puppeteer/definition.yml)"
msg_ok "Reinstalled Puppeteer"
fi
msg_info "Starting Service"
systemctl start wikijs
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
fi
msg_warn "Wiki.js 3 cannot update a 2.x install in place. Export it under Administration > Utilities > Export for Wiki.js 3.x and import it into a new Wiki.js 3 container."
NODE_VERSION="26" NODE_MODULE="yarn,node-gyp" setup_nodejs
if check_for_gh_release "wikijs" "requarks/wiki-v2"; then
+2 -2
View File
@@ -18,11 +18,11 @@ if [[ -z "${var_os:-}" ]] && command -v pveversion >/dev/null 2>&1; then
fi
if [[ "${var_os:-}" == "alpine" ]]; then
var_ram="${var_ram:-256}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-1}"
var_version="${var_version:-3.24}"
else
var_ram="${var_ram:-1024}"
var_ram="${var_ram:-512}"
var_disk="${var_disk:-4}"
var_version="${var_version:-13}"
fi
+1 -1
View File
@@ -45,7 +45,7 @@ function update_script() {
msg_info "Running Post-Update Tasks"
cd /opt/writefreely
$STD ./writefreely db migrate
ln -sf /opt/writefreely/writefreely /usr/local/bin/writefreely
ln -s /opt/writefreely/writefreely /usr/local/bin/writefreely
msg_ok "Ran Post-Update Tasks"
msg_info "Starting Services"
+1 -1
View File
@@ -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" "prebuild" "latest" "/opt/yuvomi" "yuvomi-*-dist.tar.gz"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "tarball"
NODE_VERSION="24" setup_nodejs
msg_info "Installing Node.js Dependencies"
+1 -1
View File
@@ -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:-3072}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-5}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
-3
View File
@@ -20,9 +20,6 @@ $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
+2 -8
View File
@@ -48,7 +48,6 @@ 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
@@ -63,18 +62,13 @@ expect "Email address"
send "\r"
expect "Password"
send "${ADMIN_PASS}\r"
send "community-scripts.org\r"
expect "Password (again)"
send "${ADMIN_PASS}\r"
send "community-scripts.org\r"
expect eof
EOF
{
echo "ArchiveBox Credentials"
echo "Username: archivebox"
echo "Password: ${ADMIN_PASS}"
} >~/archivebox.creds
msg_ok "Installed ArchiveBox"
msg_info "Creating Service"
+1 -8
View File
@@ -63,19 +63,12 @@ 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: "${AUTHELIA_HASH}"
password: "\$argon2id\$v=19\$m=65536,t=3,p=4\$ZBopMzXrzhHXPEZxRDVT2w\$SxWm96DwhOsZyn34DLocwQEIb4kCDsk632PuiMdZnig"
groups: []
EOF
cat <<EOF >/etc/authelia/configuration.yml
+7 -16
View File
@@ -23,15 +23,10 @@ setup_deb_based() {
setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "binary"
msg_info "Setup xCaddy"
$STD apt install -y git
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy is installed; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
}
@@ -74,14 +69,10 @@ EOF
GO_VERSION="$(curl -fsSL https://go.dev/VERSION?m=text | head -1 | cut -c3-)" setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "prebuild" "latest" "/opt/xcaddy" "xcaddy_*_linux_$(arch_resolve).tar.gz"
ln -sf /opt/xcaddy/xcaddy /usr/local/bin/xcaddy
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy is installed; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
msg_info "Enabling Caddy Service"
-54
View File
@@ -1,54 +0,0 @@
#!/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
+2 -2
View File
@@ -24,7 +24,7 @@ $STD apt install -y \
msg_ok "Installed Dependencies"
setup_ffmpeg
NODE_VERSION="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
msg_info "Building Cinephage"
@@ -35,7 +35,7 @@ $STD npm prune --omit=dev
msg_ok "Built Cinephage"
msg_info "Installing Camoufox"
$STD /opt/cinephage/node_modules/.bin/camoufox fetch
$STD /opt/cinephage/node_modules/.bin/camoufox-js fetch
msg_ok "Installed Camoufox"
msg_info "Configuring Cinephage"
+1 -1
View File
@@ -17,7 +17,7 @@ msg_info "Installing Dependencies"
$STD apt install -y build-essential
msg_ok "Installed Dependencies"
NODE_VERSION="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
msg_info "Setup Cross-Seed"
$STD npm install cross-seed@latest -g
+86 -14
View File
@@ -16,47 +16,119 @@ update_os
msg_info "Installing Dependencies"
$STD apt install -y \
nginx \
libedit2 \
libgssapi-krb5-2 \
libncurses6 \
libreadline8t64
valkey \
mariadb-client \
rclone
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 env VITE_APP_VERSION="${RELEASE}" pnpm run build
$STD pnpm run build
cd /opt/databasus/backend
$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
$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
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
APP_VERSION=${RELEASE}
# Server
SERVER_PORT=4005
SERVER_HOST=0.0.0.0
# Database
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"
@@ -71,8 +143,8 @@ msg_info "Creating Databasus Service"
cat <<EOF >/etc/systemd/system/databasus.service
[Unit]
Description=Databasus - Database Backup Management
After=network.target postgresql.service
Requires=postgresql.service
After=network.target postgresql.service valkey.service
Requires=postgresql.service valkey.service
[Service]
Type=simple
+1 -28
View File
@@ -25,36 +25,9 @@ $STD apt install -y \
gsfonts \
brotli \
nginx \
redis-server \
meson \
ninja-build \
pkg-config \
libglib2.0-dev \
libexpat1-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
libtiff-dev \
libheif-dev \
libjxl-dev \
librsvg2-dev \
libcgif-dev \
libexif-dev \
liblcms2-dev \
liborc-0.4-dev \
libimagequant-dev \
libpango1.0-dev
redis-server
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "libvips" "libvips/libvips" "prebuild" "latest" "/opt/libvips" "vips-*.tar.xz"
msg_info "Building libvips"
cd /opt/libvips
$STD meson setup build --prefix=/usr/local --libdir=lib --buildtype=release -Dmodules=disabled -Dmagick=disabled -Dexamples=false
$STD ninja -C build install
ldconfig
msg_ok "Built libvips"
msg_info "Cloning Discourse"
$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse
msg_ok "Cloned Discourse"
-146
View File
@@ -1,146 +0,0 @@
#!/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
-1
View File
@@ -26,7 +26,6 @@ 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
-99
View File
@@ -1,99 +0,0 @@
#!/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
+26 -21
View File
@@ -14,15 +14,18 @@ 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 \
@@ -30,30 +33,28 @@ $STD apt install -y \
fonts-dejavu \
fonts-noto-cjk \
fonts-noto-color-emoji \
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
fonts-noto-core
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/v9/cmd.Version=$(cat ~/.gotenberg)'" \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
cmd/gotenberg/main.go
msg_ok "Built Gotenberg"
@@ -69,10 +70,14 @@ 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=LOK_BIN_PATH=/usr/local/bin/lok
Environment=UNOCONVERTER_BIN_PATH=/usr/local/bin/unoconverter
Environment=OTEL_TRACES_EXPORTER=none
Environment=OTEL_METRICS_EXPORTER=none
Environment=OTEL_LOGS_EXPORTER=none
+1 -7
View File
@@ -33,13 +33,7 @@ msg_ok "Set up Python Backend"
msg_info "Configuring Homelable"
mkdir -p /opt/homelable/data
SECRET_KEY=$(openssl rand -hex 32)
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
BCRYPT_HASH=$(/opt/homelable/backend/.venv/bin/python -c "import bcrypt; print(bcrypt.hashpw(b'admin', bcrypt.gensalt()).decode())")
cat <<EOF >/opt/homelable/backend/.env
SECRET_KEY=${SECRET_KEY}
SQLITE_PATH=/opt/homelable/data/homelab.db
+28 -56
View File
@@ -18,12 +18,7 @@ if is_debian && [[ "$(get_os_version_major)" -lt 13 ]]; then
exit 238
fi
if [[ "$(arch_resolve)" == "amd64" ]] && ! grep -qm1 sse4_2 /proc/cpuinfo; then
msg_error "Immich needs a CPU with SSE4.2: its web build stops with 'Illegal instruction' on older ones."
exit 236
fi
RELEASE="v3.4.1"
RELEASE="v3.3.5"
if lscpu | grep -q 'GenuineIntel'; then
echo ""
echo ""
@@ -68,7 +63,6 @@ msg_info "Installing dependencies"
$STD apt install --no-install-recommends -y \
git \
redis \
pigz \
autoconf \
build-essential \
python3-dev \
@@ -114,7 +108,6 @@ $STD apt install --no-install-recommends -y \
libhwy-dev \
libwebp-dev \
libaom-dev \
unzip \
ccache
setup_deb822_repo \
@@ -136,28 +129,6 @@ if [[ "$CTTYPE" == "0" && -d /dev/dri ]]; then
fi
msg_ok "Dependencies Installed"
INSTALL_DIR="/opt/${APPLICATION}"
UPLOAD_DIR="${INSTALL_DIR}/upload"
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"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
msg_info "Installing GeoNames data"
cd "$GEO_DIR"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
rm cities500.zip
cd ~
msg_ok "Installed GeoNames data"
MISE_ARCH="x64"
[[ "$(arch_resolve)" == "arm64" ]] && MISE_ARCH="arm64"
fetch_and_deploy_gh_release "mise" "jdx/mise" "singlefile" "latest" "/usr/local/bin" "mise-v*-linux-${MISE_ARCH}"
@@ -196,7 +167,7 @@ ACTUAL_PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -1)
ACTUAL_PG_VERSION=${ACTUAL_PG_VERSION:-16}
VCHORD_RELEASE="1.1.1"
fetch_and_deploy_gh_release "VectorChord" "supervc-stack/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${ACTUAL_PG_VERSION}-vchord_*_$(arch_resolve).deb"
fetch_and_deploy_gh_release "VectorChord" "tensorchord/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${ACTUAL_PG_VERSION}-vchord_*_$(arch_resolve).deb"
sed -i "s/^#shared_preload.*/shared_preload_libraries = 'vchord.so'/" /etc/postgresql/${ACTUAL_PG_VERSION}/main/postgresql.conf
systemctl restart postgresql.service
@@ -223,7 +194,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 -- third_party
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
mkdir build
cd build
$STD cmake \
@@ -376,6 +347,15 @@ libvips: $LIBVIPS_REVISION
EOF
msg_ok "Custom Photo-processing Libraries Compiled Successfully"
INSTALL_DIR="/opt/${APPLICATION}"
UPLOAD_DIR="${INSTALL_DIR}/upload"
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"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
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
@@ -416,7 +396,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,restic,npm:@openapitools/openapi-generator-cli
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
mise_ok=0
for i in 1 2 3; do
@@ -434,8 +414,7 @@ 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)"
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"
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"
gunzip -f /tmp/extism-js.gz
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
rm -f /tmp/extism-js
@@ -447,25 +426,6 @@ 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"
@@ -484,7 +444,7 @@ if [[ -f ~/.openvino ]]; then
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -502,7 +462,7 @@ else
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -521,8 +481,20 @@ grep -rlE "'/build'" | xargs -n1 sed -i "s|'/build'|'$APP_DIR'|g"
sed -i "s@\"/cache\"@\"$INSTALL_DIR/cache\"@g" "$ML_DIR"/immich_ml/config.py
ln -s "$UPLOAD_DIR" "$APP_DIR"/upload
ln -s "$UPLOAD_DIR" "$ML_DIR"/upload
msg_info "Installing GeoNames data"
cd "$GEO_DIR"
curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
rm cities500.zip
cd "$INSTALL_DIR"
ln -s "$GEO_DIR" "$APP_DIR"
msg_ok "Installed GeoNames data"
# MickLesk temporary patch for HEIC thumbnail gen
MEDIA_REPO_JS="/opt/immich/app/dist/repositories/media.repository.js"
+1 -1
View File
@@ -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="24" NODE_MODULE="pnpm" setup_nodejs
NODE_VERSION="20" NODE_MODULE="pnpm" setup_nodejs
fetch_and_deploy_gh_tag "kan" "kanbn/kan" "latest"
+1 -9
View File
@@ -21,7 +21,6 @@ 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
@@ -41,7 +40,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=${ADMIN_PASS}"
Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=admin123"
# 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"
@@ -49,13 +48,6 @@ 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"
+1 -8
View File
@@ -40,7 +40,6 @@ 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
@@ -48,16 +47,10 @@ log_user 0
spawn bin/console kimai:user:create admin admin@community-scripts.org ROLE_SUPER_ADMIN
expect "Please enter the password:"
send "${ADMIN_PASS}\r"
send "community-scripts.org\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:
+1 -1
View File
@@ -26,7 +26,7 @@ NODE_VERSION="24" NODE_MODULE="corepack,pnpm" setup_nodejs
msg_info "Installing Deno"
export DENO_INSTALL="/usr/local"
curl -fsSL https://deno.land/install.sh | CI=1 $STD sh -s
curl -fsSL https://deno.land/install.sh | $STD sh -s -- -y
[[ ":$PATH:" != *":/usr/local/bin:"* ]] &&
echo -e "\nexport PATH=\"/usr/local/bin:\$PATH\"" >>~/.bashrc &&
source ~/.bashrc
+1 -7
View File
@@ -33,13 +33,7 @@ $STD yarn config set ignore-engines true
$STD yarn install
$STD yarn run production
$STD php artisan key:generate
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
$STD php artisan setup:production --email=admin@community-scripts.org --password=community-scripts.org --force
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
+1 -1
View File
@@ -13,7 +13,7 @@ setting_up_container
network_check
update_os
NODE_VERSION="26" setup_nodejs
NODE_VERSION="24" setup_nodejs
fetch_and_deploy_gh_release "myip" "jason5ng32/MyIP" "tarball"
msg_info "Configuring MyIP"
+1 -4
View File
@@ -23,10 +23,7 @@ msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
msg_info "Installing n8n (Patience)"
# n8n-nodes-base 2.46.0 pins both @napi-rs/canvas musl binaries as hard
# dependencies, which no os/cpu/libc combination can satisfy. Drop --force
# once upstream makes them optional again.
$STD npm install -g n8n@latest --force
$STD npm install -g n8n@latest
msg_ok "Installed n8n"
msg_info "Creating Service"
+1 -7
View File
@@ -63,13 +63,7 @@ setcap cap_net_raw+p /bin/ping
msg_ok "Built Nagios Plugins"
msg_info "Configuring Web Authentication"
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
$STD htpasswd -bc /usr/local/nagios/etc/htpasswd.users nagiosadmin nagiosadmin
chown root:www-data /usr/local/nagios/etc/htpasswd.users
chmod 640 /usr/local/nagios/etc/htpasswd.users
msg_ok "Configured Web Authentication"
+2 -9
View File
@@ -201,13 +201,6 @@ 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"
@@ -226,8 +219,8 @@ desktop:
member:
provider: "multiuser"
multiuser:
admin_password: "${NEKO_ADMIN_PASS}"
user_password: "${NEKO_USER_PASS}"
admin_password: "admin"
user_password: "neko"
EOF
msg_ok "Set up Runtime"
-1
View File
@@ -33,7 +33,6 @@ LOCAL_ENGINE_TOKEN=${LOCAL_ENGINE_TOKEN}
ENCRYPTION_KEY=${ENCRYPTION_KEY}
EOF
chmod 0640 /etc/nexterm-engine/config.yaml /etc/nexterm-server/server.env
echo "x /tmp/nexterm-sea-*" >/etc/tmpfiles.d/nexterm.conf
msg_ok "Configured Nexterm"
msg_info "Creating Services"
+2 -5
View File
@@ -31,7 +31,6 @@ 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
@@ -39,8 +38,6 @@ 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
@@ -100,10 +97,10 @@ expect "Administrator email address" {
send "admin@community-scripts.org\r"
}
expect "Password" {
send "$NODEBB_ADMIN_PASS\r"
send "community-scripts\r"
}
expect "Confirm Password" {
send "$NODEBB_ADMIN_PASS\r"
send "community-scripts\r"
}
expect eof
EOF
+1 -7
View File
@@ -57,17 +57,11 @@ 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=${ADMIN_PASS}\"])
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=community-scripts.org\"])
" /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
+1 -7
View File
@@ -30,13 +30,7 @@ rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
msg_ok "Setup Python3"
msg_info "Creating user 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
useradd -m -s /bin/bash -p $(openssl passwd -1 octoprint) octoprint
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
+3 -2
View File
@@ -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 --no-same-owner
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
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,6 +38,7 @@ After=network.target
Type=simple
EnvironmentFile=/opt/openobserve/data/.env
ExecStart=/opt/openobserve/openobserve
ExecStop=killall -QUIT openobserve
Restart=on-failure
[Install]
-3
View File
@@ -70,9 +70,6 @@ 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
+73 -9
View File
@@ -13,36 +13,100 @@ 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
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
$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
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
After=network.target paperless-rag.service
Requires=paperless-rag.service
[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
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
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"
+1 -7
View File
@@ -43,13 +43,7 @@ $STD php artisan migrate --force --seed
msg_ok "Set up database"
msg_info "Creating Admin User"
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
$STD php artisan app:user:create paymenter admin admin@paymenter.org paymenter 1 -q
msg_ok "Created Admin User"
msg_info "Configuring Nginx"
+2 -8
View File
@@ -46,16 +46,10 @@ 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='${ADMIN_PASS}'
PHOTOPRISM_ADMIN_PASSWORD='changeme'
PHOTOPRISM_AUTH_MODE='password'
PHOTOPRISM_PUBLIC='false'
@@ -118,7 +112,7 @@ StoragePath: "/opt/photoprism/storage"
OriginalsPath: "/opt/photoprism/photos/originals"
ImportPath: "/media"
AdminUser: "admin"
AdminPassword: "${ADMIN_PASS}"
AdminPassword: "changeme"
AuthMode: "password"
DatabaseDriver: "sqlite"
HttpHost: "0.0.0.0"
-232
View File
@@ -1,232 +0,0 @@
#!/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
+1 -3
View File
@@ -70,9 +70,7 @@ After=network.target
[Service]
WorkingDirectory=/opt/planka
Environment=NODE_ENV=production
Environment=HOME=/root
ExecStart=/usr/bin/node app.js --prod
ExecStart=/usr/bin/npm start --prod
Restart=always
[Install]
+2 -4
View File
@@ -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/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
@@ -13,9 +13,7 @@ setting_up_container
network_check
update_os
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
fetch_and_deploy_gh_release "pocketbase" "pocketbase/pocketbase" "prebuild" "latest" "/opt/pocketbase" "pocketbase*linux_$(arch_resolve).zip"
msg_info "Configuring Pocketbase"
mkdir -p /opt/pocketbase/{pb_public,pb_migrations,pb_hooks}
-4
View File
@@ -48,10 +48,6 @@ msg_ok "Installed Podman"
mkdir -p /etc/containers/systemd
if [[ ! -x /usr/libexec/podman/quadlet ]]; then
msg_warn "Portainer runs as a Quadlet unit, which needs Podman 4.4 or newer (Debian 13); skipping it"
var_podman_portainer="n" var_podman_portainer_agent="n"
fi
prompt="${var_podman_portainer:-}"
[[ -n "$prompt" ]] || read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt || true
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
+2 -2
View File
@@ -16,9 +16,9 @@ update_os
msg_info "Installing Proxmox Mail Gateway"
setup_deb822_repo \
"pmg" \
"https://enterprise.proxmox.com/debian/proxmox-release-$(get_os_info codename).gpg" \
"https://enterprise.proxmox.com/debian/proxmox-release-trixie.gpg" \
"http://download.proxmox.com/debian/pmg" \
"$(get_os_info codename)" \
"trixie" \
"pmg-no-subscription"
$STD apt install -y proxmox-mailgateway-container
msg_ok "Installed Proxmox Mail Gateway"
+1 -9
View File
@@ -18,20 +18,12 @@ 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(${QB_HASH})"
WebUI\Password_PBKDF2="@ByteArray(amjeuVrF3xRbgzqWQmes5A==:XK3/Ra9jUmqUc4RwzCtrhrkQIcYczBl90DJw2rT8DFVTss4nxpoRhvyxhCf87ahVE3SzD8K9lyPdpyUCfmVsUg==)"
WebUI\Port=8090
WebUI\UseUPnP=false
WebUI\Username=admin
+1 -7
View File
@@ -43,15 +43,9 @@ $STD rabbitmqctl enable_feature_flag all
msg_ok "Enabled RabbitMQ Management Plugin"
msg_info "Creating User"
RABBITMQ_PASS=$(random_password 20)
$STD rabbitmqctl add_user proxmox "$RABBITMQ_PASS"
$STD rabbitmqctl add_user proxmox proxmox
$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
+8 -19
View File
@@ -257,33 +257,15 @@ 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 - also emits the player isolation headers: the player URLs
# fall through try_files to /index.html, which this location serves
# Frontend SPA
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
@@ -294,6 +276,13 @@ 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;
+1 -1
View File
@@ -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=$(openssl rand -hex 32)
SIGNOZ_JWT_SECRET=secret
SIGNOZ_ALERTMANAGER_PROVIDER=signoz
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://localhost:9000?password=
+1 -7
View File
@@ -78,12 +78,6 @@ 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
@@ -98,7 +92,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=${SNAPOTTER_PASS}
DEFAULT_PASSWORD=admin
LOG_LEVEL=info
TRUST_PROXY=true
FILE_MAX_AGE_HOURS=72
+4
View File
@@ -116,6 +116,10 @@ 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"
+4 -6
View File
@@ -17,8 +17,7 @@ msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
python3 \
git \
redis-server
git
msg_ok "Installed Dependencies"
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
@@ -57,8 +56,8 @@ SECRET_KEY=${SECRET_KEY}
PORT=3000
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
BACKEND_CACHE_PROVIDER=redis
BACKEND_CACHE_REDIS_URI=redis://127.0.0.1:6379/0
BACKEND_CACHE_PROVIDER=sqlite
BACKEND_CACHE_SQLITE_URI=sqlite:///opt/teable/.assets/.cache.db
NEXTJS_DIR=apps/nextjs-app
EOF
ln -sf /opt/teable /app
@@ -71,11 +70,10 @@ fi
msg_ok "Configured Teable"
msg_info "Creating Service"
systemctl enable -q --now redis-server
cat <<EOF >/etc/systemd/system/teable.service
[Unit]
Description=Teable
After=network.target postgresql.service redis-server.service
After=network.target postgresql.service
[Service]
Type=simple
+1 -1
View File
@@ -23,7 +23,7 @@ setup_deb822_repo \
$STD apt install -y aspnetcore-runtime-10.0
msg_ok "Installed Dependencies"
RELEASE=$(get_latest_github_release "TechnitiumSoftware/DnsServer")
RELEASE=$(curl -fsSL https://technitium.com/dns/ | grep -oP 'Version \K[\d.]+')
fetch_and_deploy_from_url "https://download.technitium.com/dns/DnsServerPortable.tar.gz" /opt/technitium/dns
echo "${RELEASE}" >~/.technitium
+1 -5
View File
@@ -24,11 +24,7 @@ msg_info "Configuring Teleport"
$STD apt install -y teleport
$STD teleport configure -o /etc/teleport.yaml
systemctl enable -q --now teleport
SECONDS=0
until teleport debug readyz &>/dev/null; do
sleep 2
((SECONDS >= 120)) && { msg_error "Teleport did not become ready in time"; exit 1; }
done
sleep 10
tctl users add teleport-admin --roles=editor,access --logins=root >~/teleportadmin.txt
sed -i "s|https://[^:]*:3080|https://${LOCAL_IP}:3080|g" ~/teleportadmin.txt
msg_ok "Configured Teleport"
+1 -2
View File
@@ -26,8 +26,7 @@ PG_DB_NAME="thingsboard_db" PG_DB_USER="thingsboard" setup_postgresql_db
fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
msg_info "Configuring ThingsBoard"
cat <<EOF >>/etc/thingsboard/conf/thingsboard.conf
cat <<EOF >/etc/thingsboard/conf/thingsboard.conf
# DB Configuration
export DATABASE_TS_TYPE=sql
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/${PG_DB_NAME}
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more