Compare commits
No files matched your search
@@ -58,9 +58,9 @@ body:
|
||||
|
||||
Before continuing, verify that the requested project meets the minimum requirements below.
|
||||
|
||||
**Projects with fewer than 1,000 stars are generally not eligible for a script request.**
|
||||
**Projects with fewer than 1,000 stars, or younger than 6 months, are generally not eligible for a script request.**
|
||||
|
||||
Exceptions are only considered where there is a clearly verifiable, significant public adoption signal.
|
||||
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.
|
||||
|
||||
- type: input
|
||||
id: repository-stars
|
||||
@@ -134,6 +134,22 @@ 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: |
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Community Scripts - CS Amber</title>
|
||||
<desc id="desc">A continuous ivory C interlocks with a warm orange S on charcoal. Both letterforms use rounded, folded geometry.</desc>
|
||||
<rect width="512" height="512" rx="112" fill="#29262b"/>
|
||||
<defs>
|
||||
<mask id="weave-clearance" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512">
|
||||
<rect width="512" height="512" fill="white"/>
|
||||
<path d="M390 142H322L270 172V220L322 250H348L392 276V338L338 370H284" fill="none" stroke="black" stroke-width="72" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g fill="none" stroke-width="56" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M254 142H186L118 182V330L186 370H254" stroke="#f5f4eb" mask="url(#weave-clearance)"/>
|
||||
<path d="M390 142H322L270 172V220L322 250H348L392 276V338L338 370H284" stroke="#f29a67"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 997 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@@ -77,17 +77,6 @@
|
||||
"excludeGlobs": []
|
||||
}
|
||||
],
|
||||
"core": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"misc/*.func"
|
||||
],
|
||||
"excludeGlobs": [
|
||||
"misc/api.func"
|
||||
]
|
||||
}
|
||||
],
|
||||
"documentation": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
@@ -114,8 +103,7 @@
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"api/**",
|
||||
"misc/api.func"
|
||||
"api/**"
|
||||
],
|
||||
"excludeGlobs": []
|
||||
}
|
||||
|
||||
@@ -1,3 +1,731 @@
|
||||
## 2026-10-31
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Docker VM: take the imported disk from vm_import_disk [@MickLesk](https://github.com/MickLesk) ([#18341](https://github.com/community-scripts/ProxmoxVE/pull/18341))
|
||||
- Twenty: move to Node.js 26 [@MickLesk](https://github.com/MickLesk) ([#18337](https://github.com/community-scripts/ProxmoxVE/pull/18337))
|
||||
- Norish: fetch Obscura from the repo Norish pins [@MickLesk](https://github.com/MickLesk) ([#18338](https://github.com/community-scripts/ProxmoxVE/pull/18338))
|
||||
- HAOS VM: take the imported disk from vm_import_disk [@MickLesk](https://github.com/MickLesk) ([#18342](https://github.com/community-scripts/ProxmoxVE/pull/18342))
|
||||
- Aurral: build the pnpm workspace and run backend/server.ts [@MickLesk](https://github.com/MickLesk) ([#18340](https://github.com/community-scripts/ProxmoxVE/pull/18340))
|
||||
- PS5-MQTT: build the yarn workspace and run the packaged server [@MickLesk](https://github.com/MickLesk) ([#18339](https://github.com/community-scripts/ProxmoxVE/pull/18339))
|
||||
- Jellyseerr, Overseerr, Paperless-ngx, Vikunja: skip migration prompts when unattended [@florianpunz](https://github.com/florianpunz) ([#18321](https://github.com/community-scripts/ProxmoxVE/pull/18321))
|
||||
- Discourse: track the Ruby version Discourse requires [@MickLesk](https://github.com/MickLesk) ([#18329](https://github.com/community-scripts/ProxmoxVE/pull/18329))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- MatterJS-Server: Add time sync [@tomfrenzel](https://github.com/tomfrenzel) ([#18330](https://github.com/community-scripts/ProxmoxVE/pull/18330))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Debian VM: keep a static IP without a snippets storage [@MickLesk](https://github.com/MickLesk) ([#18346](https://github.com/community-scripts/ProxmoxVE/pull/18346))
|
||||
- Ubuntu VM: check for a snippets storage before building a static-IP VM [@MickLesk](https://github.com/MickLesk) ([#18347](https://github.com/community-scripts/ProxmoxVE/pull/18347))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: add setup_java_memory: size Java services from the CT's RAM on Proxmox [@MickLesk](https://github.com/MickLesk) ([core#181](https://github.com/community-scripts/core/pull/181))
|
||||
- analytics: name the Proxmox tasks when a container stops mid-install [@MickLesk](https://github.com/MickLesk) ([core#184](https://github.com/community-scripts/core/pull/184))
|
||||
- PVE: do not abort when the spinner PID file cannot be written [@MickLesk](https://github.com/MickLesk) ([core#183](https://github.com/community-scripts/core/pull/183))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Drop the legacy engine copies from misc/ [@MickLesk](https://github.com/MickLesk) ([#18294](https://github.com/community-scripts/ProxmoxVE/pull/18294))
|
||||
|
||||
## 2026-10-30
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Reconya ([#18325](https://github.com/community-scripts/ProxmoxVE/pull/18325))
|
||||
- Blinko ([#18324](https://github.com/community-scripts/ProxmoxVE/pull/18324))
|
||||
- ChartDB ([#18323](https://github.com/community-scripts/ProxmoxVE/pull/18323))
|
||||
|
||||
## 2026-10-29
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Karakeep: fix monolith release asset pattern [@MohamedBassem](https://github.com/MohamedBassem) ([#18317](https://github.com/community-scripts/ProxmoxVE/pull/18317))
|
||||
- Vikunja: fold the package's service block into the config's service section [@MickLesk](https://github.com/MickLesk) ([#18311](https://github.com/community-scripts/ProxmoxVE/pull/18311))
|
||||
- Stirling-PDF: size JVM heap from container memory, not host RAM [@aashishvanand](https://github.com/aashishvanand) ([#18292](https://github.com/community-scripts/ProxmoxVE/pull/18292))
|
||||
- fix(authentik): migrate systemd units when still on pre-2026.8 paths [@CervezaStallone](https://github.com/CervezaStallone) ([#18307](https://github.com/community-scripts/ProxmoxVE/pull/18307))
|
||||
- fix(dawarich): serve .mjs MapLibre modules as JavaScript [@ricbaral](https://github.com/ricbaral) ([#18304](https://github.com/community-scripts/ProxmoxVE/pull/18304))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- VictoriaLogs: enable syslog ingestion on TCP/UDP 514 [@opastorello](https://github.com/opastorello) ([#18300](https://github.com/community-scripts/ProxmoxVE/pull/18300))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: Performance - load host data in the background and stop reporting failed pre-checks [@MickLesk](https://github.com/MickLesk) ([core#179](https://github.com/community-scripts/core/pull/179))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- gh: let the bot set oidc_url and license [@MickLesk](https://github.com/MickLesk) ([#18313](https://github.com/community-scripts/ProxmoxVE/pull/18313))
|
||||
|
||||
## 2026-10-28
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- VictoriaLogs ([#18282](https://github.com/community-scripts/ProxmoxVE/pull/18282))
|
||||
- Deemix ([#18272](https://github.com/community-scripts/ProxmoxVE/pull/18272))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(vm/opnsense): increase startup wait from 90 to 180 seconds [@MTlyx](https://github.com/MTlyx) ([#18086](https://github.com/community-scripts/ProxmoxVE/pull/18086))
|
||||
- apache-tomcat: restore the update backup from its real store [@mazelado](https://github.com/mazelado) ([#18145](https://github.com/community-scripts/ProxmoxVE/pull/18145))
|
||||
- AdventureLog: clean-install on update and drop the DRF <3.15 pin [@angusmaul](https://github.com/angusmaul) ([#18281](https://github.com/community-scripts/ProxmoxVE/pull/18281))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: ownCloud VM [@MickLesk](https://github.com/MickLesk) ([#18295](https://github.com/community-scripts/ProxmoxVE/pull/18295))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep the console hint out of the default output [@MickLesk](https://github.com/MickLesk) ([core#176](https://github.com/community-scripts/core/pull/176))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Move a promoted script's telemetry history to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([#18271](https://github.com/community-scripts/ProxmoxVE/pull/18271))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- chore(ct): sync tdarr defaults with PocketBase [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18289](https://github.com/community-scripts/ProxmoxVE/pull/18289))
|
||||
|
||||
## 2026-10-27
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Zabbix: leave out the NVIDIA agent2 plugin that breaks the all-plugins install [@MickLesk](https://github.com/MickLesk) ([#18262](https://github.com/community-scripts/ProxmoxVE/pull/18262))
|
||||
- Omada: detect latest version from product download page [@ChrisKappe](https://github.com/ChrisKappe) ([#18248](https://github.com/community-scripts/ProxmoxVE/pull/18248))
|
||||
- LanguageTool: count ngram prompt retries without tripping errexit [@MickLesk](https://github.com/MickLesk) ([#18258](https://github.com/community-scripts/ProxmoxVE/pull/18258))
|
||||
- Overseerr: point fresh installs to Seerr [@MickLesk](https://github.com/MickLesk) ([#18259](https://github.com/community-scripts/ProxmoxVE/pull/18259))
|
||||
- stalwart: pin to v0.16.27 until v1 migration is supported [@MickLesk](https://github.com/MickLesk) ([#18252](https://github.com/community-scripts/ProxmoxVE/pull/18252))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- TurnKey: offer only current appliances, stop on arm64 [@MickLesk](https://github.com/MickLesk) ([#18265](https://github.com/community-scripts/ProxmoxVE/pull/18265))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- NPMplus: let the ACME e-mail stay empty again, Let's Encrypt does not need one [@MickLesk](https://github.com/MickLesk) ([#18263](https://github.com/community-scripts/ProxmoxVE/pull/18263))
|
||||
|
||||
### 🗑️ Deleted Scripts
|
||||
|
||||
- Remove: Readarr [@MickLesk](https://github.com/MickLesk) ([#18264](https://github.com/community-scripts/ProxmoxVE/pull/18264))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Say the same thing on both hosts while a container is created [@MickLesk](https://github.com/MickLesk) ([core#173](https://github.com/community-scripts/core/pull/173))
|
||||
- Skip the IPv6 sysctl when the host kernel has no IPv6 [@MickLesk](https://github.com/MickLesk) ([core#171](https://github.com/community-scripts/core/pull/171))
|
||||
- Stop reporting a closed console as a user abort, and shield the host's own work [@MickLesk](https://github.com/MickLesk) ([core#172](https://github.com/community-scripts/core/pull/172))
|
||||
|
||||
## 2026-10-26
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Vane ([#18168](https://github.com/community-scripts/ProxmoxVE/pull/18168))
|
||||
- HomeTube ([#18172](https://github.com/community-scripts/ProxmoxVE/pull/18172))
|
||||
- Honcho ([#18169](https://github.com/community-scripts/ProxmoxVE/pull/18169))
|
||||
- YTPTube ([#18174](https://github.com/community-scripts/ProxmoxVE/pull/18174))
|
||||
- Stump ([#18173](https://github.com/community-scripts/ProxmoxVE/pull/18173))
|
||||
- Casdoor ([#18170](https://github.com/community-scripts/ProxmoxVE/pull/18170))
|
||||
- Streamystats ([#18171](https://github.com/community-scripts/ProxmoxVE/pull/18171))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Immich: lock only this platform when mise installs the plugin tools [@MickLesk](https://github.com/MickLesk) ([#18241](https://github.com/community-scripts/ProxmoxVE/pull/18241))
|
||||
- Fix service name case for Splunkd [@CerberusStyle](https://github.com/CerberusStyle) ([#18238](https://github.com/community-scripts/ProxmoxVE/pull/18238))
|
||||
- VMs: pass the machine type, CPU, disk cache and discard settings to qm [@MickLesk](https://github.com/MickLesk) ([#18232](https://github.com/community-scripts/ProxmoxVE/pull/18232))
|
||||
- excalidash: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18213](https://github.com/community-scripts/ProxmoxVE/pull/18213))
|
||||
- readmeabook: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18217](https://github.com/community-scripts/ProxmoxVE/pull/18217))
|
||||
- homepage: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18214](https://github.com/community-scripts/ProxmoxVE/pull/18214))
|
||||
- remove old -install.sh (verdaccio) [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18218](https://github.com/community-scripts/ProxmoxVE/pull/18218))
|
||||
- meshcentral: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18215](https://github.com/community-scripts/ProxmoxVE/pull/18215))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- MeshCentral: update the npm package, not just the OS [@MickLesk](https://github.com/MickLesk) ([#18243](https://github.com/community-scripts/ProxmoxVE/pull/18243))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: let an install survive a closed console / dont sighup installs [@MickLesk](https://github.com/MickLesk) ([core#167](https://github.com/community-scripts/core/pull/167))
|
||||
- feat: create containers from the linuxcontainers.org catalog when Proxmox has no template [@MickLesk](https://github.com/MickLesk) ([core#25](https://github.com/community-scripts/core/pull/25))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Ref: Docs / Readme [@MickLesk](https://github.com/MickLesk) ([#18233](https://github.com/community-scripts/ProxmoxVE/pull/18233))
|
||||
|
||||
## 2026-10-25
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Dreeve ([#18167](https://github.com/community-scripts/ProxmoxVE/pull/18167))
|
||||
- TaskView ([#18166](https://github.com/community-scripts/ProxmoxVE/pull/18166))
|
||||
- EmDash ([#18165](https://github.com/community-scripts/ProxmoxVE/pull/18165))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(mealie): install ffmpeg for video recipe import [@clartek](https://github.com/clartek) ([#18210](https://github.com/community-scripts/ProxmoxVE/pull/18210))
|
||||
- Termix: step through 2.9.2 when updating from 2.8 or older [@MickLesk](https://github.com/MickLesk) ([#18158](https://github.com/community-scripts/ProxmoxVE/pull/18158))
|
||||
- Element Synapse: remove the registration line by its text, not line 34 [@MickLesk](https://github.com/MickLesk) ([#18201](https://github.com/community-scripts/ProxmoxVE/pull/18201))
|
||||
- Element Synapse: drop the ::1 listener so a container without IPv6 starts [@MickLesk](https://github.com/MickLesk) ([#18180](https://github.com/community-scripts/ProxmoxVE/pull/18180))
|
||||
- Heimdall: generate the app key without the production prompt [@MickLesk](https://github.com/MickLesk) ([#18156](https://github.com/community-scripts/ProxmoxVE/pull/18156))
|
||||
- Frigate: fetch the Intel graphics key with retries before building the media driver [@MickLesk](https://github.com/MickLesk) ([#18192](https://github.com/community-scripts/ProxmoxVE/pull/18192))
|
||||
- Pterodactyl Panel: default the admin's first and last name when left empty [@MickLesk](https://github.com/MickLesk) ([#18177](https://github.com/community-scripts/ProxmoxVE/pull/18177))
|
||||
- Jellyseerr: point fresh installs to Seerr instead of a missing install script [@MickLesk](https://github.com/MickLesk) ([#18183](https://github.com/community-scripts/ProxmoxVE/pull/18183))
|
||||
- Pi-hole: download the installer before running it [@MickLesk](https://github.com/MickLesk) ([#18182](https://github.com/community-scripts/ProxmoxVE/pull/18182))
|
||||
- OmniTools: give the build a git commit to read [@MickLesk](https://github.com/MickLesk) ([#18181](https://github.com/community-scripts/ProxmoxVE/pull/18181))
|
||||
- iVentoy: stop early in an unprivileged container [@MickLesk](https://github.com/MickLesk) ([#18179](https://github.com/community-scripts/ProxmoxVE/pull/18179))
|
||||
- Immich: stop early on Debian 12 instead of failing at apt [@MickLesk](https://github.com/MickLesk) ([#18178](https://github.com/community-scripts/ProxmoxVE/pull/18178))
|
||||
- paperless-ngx: fix missing index folder [@Segelzwerg](https://github.com/Segelzwerg) ([#18142](https://github.com/community-scripts/ProxmoxVE/pull/18142))
|
||||
- Anytype Server: keep the .env across updates and recreate a lost one [@MickLesk](https://github.com/MickLesk) ([#18155](https://github.com/community-scripts/ProxmoxVE/pull/18155))
|
||||
- changedetection: build Browserless from its lockfile on update [@MickLesk](https://github.com/MickLesk) ([#18157](https://github.com/community-scripts/ProxmoxVE/pull/18157))
|
||||
- Zitadel: identify the machine by hostname when it has no private IPv4 [@MickLesk](https://github.com/MickLesk) ([#18139](https://github.com/community-scripts/ProxmoxVE/pull/18139))
|
||||
- fix(bar-assistant): publish starter media catalog so new bars provision [@och29](https://github.com/och29) ([#18130](https://github.com/community-scripts/ProxmoxVE/pull/18130))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Gramps-Web: run the Celery task queue with Redis [@MickLesk](https://github.com/MickLesk) ([#18141](https://github.com/community-scripts/ProxmoxVE/pull/18141))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Hermes Agent: install, update and run everything as the hermes user [@MickLesk](https://github.com/MickLesk) ([#18203](https://github.com/community-scripts/ProxmoxVE/pull/18203))
|
||||
- tubearchivist: fix: prepare tubearchivist for v0.6.0 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#17867](https://github.com/community-scripts/ProxmoxVE/pull/17867))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- TREK: build 5.0 with 4 GB RAM and without dependency install scripts [@MickLesk](https://github.com/MickLesk) ([#18194](https://github.com/community-scripts/ProxmoxVE/pull/18194))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- alpine: fix uv release url [@mfelipe](https://github.com/mfelipe) ([core#168](https://github.com/community-scripts/core/pull/168))
|
||||
- Set npm's script and git policy for root too, so --prefix keeps it [@MickLesk](https://github.com/MickLesk) ([core#170](https://github.com/community-scripts/core/pull/170))
|
||||
- Upgrade Node.js in place when an app package depends on it [@MickLesk](https://github.com/MickLesk) ([core#169](https://github.com/community-scripts/core/pull/169))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- post-pmg-install: hide the subscription nag in the PMG 9 mobile quarantine [@MickLesk](https://github.com/MickLesk) ([#18140](https://github.com/community-scripts/ProxmoxVE/pull/18140))
|
||||
- post-pve-install: hide the mobile subscription nag in every UI language [@MickLesk](https://github.com/MickLesk) ([#18143](https://github.com/community-scripts/ProxmoxVE/pull/18143))
|
||||
|
||||
## 2026-10-24
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- patchmon: avoids systemd environment secret leaks via D-Bus/subprocesses. [@rhclayto](https://github.com/rhclayto) ([#18074](https://github.com/community-scripts/ProxmoxVE/pull/18074))
|
||||
- fix: Linkwarden - clear Yarn berry cache after install and update- #18115 [@bferd](https://github.com/bferd) ([#18116](https://github.com/community-scripts/ProxmoxVE/pull/18116))
|
||||
- Borg-UI: recreate the borg venvs with --clear [@MickLesk](https://github.com/MickLesk) ([#18118](https://github.com/community-scripts/ProxmoxVE/pull/18118))
|
||||
- SnapOtter: recreate the AI venv with --clear [@MickLesk](https://github.com/MickLesk) ([#18119](https://github.com/community-scripts/ProxmoxVE/pull/18119))
|
||||
- Update authentik version to 2026.8.5 [@thieneret](https://github.com/thieneret) ([#18125](https://github.com/community-scripts/ProxmoxVE/pull/18125))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Frigate: bump to v0.18.2 [@MickLesk](https://github.com/MickLesk) ([#18120](https://github.com/community-scripts/ProxmoxVE/pull/18120))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- install: answer every prompt from the environment, survive a closed stdin [@MickLesk](https://github.com/MickLesk) ([#18121](https://github.com/community-scripts/ProxmoxVE/pull/18121))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(addons): use catch_errors so the update marker is cleared [@mazelado](https://github.com/mazelado) ([#18128](https://github.com/community-scripts/ProxmoxVE/pull/18128))
|
||||
|
||||
## 2026-10-23
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- postgresql: default the version prompt and match Alpine's packages [@MickLesk](https://github.com/MickLesk) ([#18093](https://github.com/community-scripts/ProxmoxVE/pull/18093))
|
||||
- Storyteller: build the runtime like upstream's Dockerfile [@MickLesk](https://github.com/MickLesk) ([#18083](https://github.com/community-scripts/ProxmoxVE/pull/18083))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- proxcenter: skip the type check and lower the build heap [@MickLesk](https://github.com/MickLesk) ([#18090](https://github.com/community-scripts/ProxmoxVE/pull/18090))
|
||||
- hermesagent: retry the installer download, fall back to GitHub [@MickLesk](https://github.com/MickLesk) ([#18091](https://github.com/community-scripts/ProxmoxVE/pull/18091))
|
||||
- zabbix: use the arm64 repository for 7.0 [@MickLesk](https://github.com/MickLesk) ([#18089](https://github.com/community-scripts/ProxmoxVE/pull/18089))
|
||||
- kasm: survive a failed release lookup and answer the installer with flags [@MickLesk](https://github.com/MickLesk) ([#18092](https://github.com/community-scripts/ProxmoxVE/pull/18092))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- InvoiceShelf: run the Laravel scheduler [@MickLesk](https://github.com/MickLesk) ([#18079](https://github.com/community-scripts/ProxmoxVE/pull/18079))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- podman-homeassistant: require Debian 13 [@MickLesk](https://github.com/MickLesk) ([#18095](https://github.com/community-scripts/ProxmoxVE/pull/18095))
|
||||
- apache-guacamole: require Debian 13 [@MickLesk](https://github.com/MickLesk) ([#18094](https://github.com/community-scripts/ProxmoxVE/pull/18094))
|
||||
- unifi-os-server: require Debian 13 [@MickLesk](https://github.com/MickLesk) ([#18096](https://github.com/community-scripts/ProxmoxVE/pull/18096))
|
||||
- openwebui: drop the Intel oneAPI Base Toolkit [@MickLesk](https://github.com/MickLesk) ([#18098](https://github.com/community-scripts/ProxmoxVE/pull/18098))
|
||||
- ollama: drop the Intel oneAPI Base Toolkit [@MickLesk](https://github.com/MickLesk) ([#18097](https://github.com/community-scripts/ProxmoxVE/pull/18097))
|
||||
- tdarr: do not let a failed or slow updater block the service start [@MickLesk](https://github.com/MickLesk) ([#18100](https://github.com/community-scripts/ProxmoxVE/pull/18100))
|
||||
- immich: install mise from its GitHub release, retry jellyfin-ffmpeg, default the ML prompt [@MickLesk](https://github.com/MickLesk) ([#18099](https://github.com/community-scripts/ProxmoxVE/pull/18099))
|
||||
- Refactor: pimox-haos-vm [@MickLesk](https://github.com/MickLesk) ([#18077](https://github.com/community-scripts/ProxmoxVE/pull/18077))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Telemetry: explain and categorize by the command that failed [@MickLesk](https://github.com/MickLesk) ([core#166](https://github.com/community-scripts/core/pull/166))
|
||||
- error-handler: report a handled silent failure only on its own line [@MickLesk](https://github.com/MickLesk) ([core#165](https://github.com/community-scripts/core/pull/165))
|
||||
- VM silent: return the failure instead of exiting [@MickLesk](https://github.com/MickLesk) ([core#164](https://github.com/community-scripts/core/pull/164))
|
||||
- error-handler: keep the start explanation and the final IP lookup from failing the run [@MickLesk](https://github.com/MickLesk) ([core#163](https://github.com/community-scripts/core/pull/163))
|
||||
- error output: report a failure inside a subshell once [@MickLesk](https://github.com/MickLesk) ([core#162](https://github.com/community-scripts/core/pull/162))
|
||||
- general: allow Composer as root in every script [@MickLesk](https://github.com/MickLesk) ([core#161](https://github.com/community-scripts/core/pull/161))
|
||||
- lxc-updater: skip the resource check when the container cannot read its RAM [@MickLesk](https://github.com/MickLesk) ([core#160](https://github.com/community-scripts/core/pull/160))
|
||||
- vm-core: let vm_timezone_default survive a host without /etc/timezone [@MickLesk](https://github.com/MickLesk) ([core#159](https://github.com/community-scripts/core/pull/159))
|
||||
|
||||
## 2026-10-22
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Zabbix: keep unattended updates on the installed series [@florianpunz](https://github.com/florianpunz) ([#18051](https://github.com/community-scripts/ProxmoxVE/pull/18051))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Cryptpad [@MickLesk](https://github.com/MickLesk) ([#18065](https://github.com/community-scripts/ProxmoxVE/pull/18065))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Incus: drive a remote Incus server from the client's default remote [@MickLesk](https://github.com/MickLesk) ([core#154](https://github.com/community-scripts/core/pull/154))
|
||||
- Deploy the release again when an update is redone [@MickLesk](https://github.com/MickLesk) ([core#158](https://github.com/community-scripts/core/pull/158))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- chore(ct): sync sparkyfitness defaults with PocketBase [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18061](https://github.com/community-scripts/ProxmoxVE/pull/18061))
|
||||
|
||||
## 2026-10-21
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Immich: bump release version to v3.3.5 (CVE) [@CerberusStyle](https://github.com/CerberusStyle) ([#18057](https://github.com/community-scripts/ProxmoxVE/pull/18057))
|
||||
- DroppedNeedle: pass release version to the app via COMMIT_TAG [@MickLesk](https://github.com/MickLesk) ([#18042](https://github.com/community-scripts/ProxmoxVE/pull/18042))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep the version an update started from across a redo [@MickLesk](https://github.com/MickLesk) ([core#156](https://github.com/community-scripts/core/pull/156))
|
||||
- Redo an update whose run did not finish after the deploy [@MickLesk](https://github.com/MickLesk) ([core#155](https://github.com/community-scripts/core/pull/155))
|
||||
|
||||
## 2026-10-20
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Plane: give Silo its own HOME [@MickLesk](https://github.com/MickLesk) ([#18028](https://github.com/community-scripts/ProxmoxVE/pull/18028))
|
||||
- Immich: Pin version to 3.3.4 [@vhsdream](https://github.com/vhsdream) ([#18036](https://github.com/community-scripts/ProxmoxVE/pull/18036))
|
||||
- InvenTree: run apt under systemd so postinst can finish [@MickLesk](https://github.com/MickLesk) ([#18029](https://github.com/community-scripts/ProxmoxVE/pull/18029))
|
||||
- Profilarr: pass the deno pin as the version [@MickLesk](https://github.com/MickLesk) ([#18025](https://github.com/community-scripts/ProxmoxVE/pull/18025))
|
||||
- Silo: keep the version file out of ~/.silo [@MickLesk](https://github.com/MickLesk) ([#18030](https://github.com/community-scripts/ProxmoxVE/pull/18030))
|
||||
- InvenTree: move to go.packager.io [@MickLesk](https://github.com/MickLesk) ([#18015](https://github.com/community-scripts/ProxmoxVE/pull/18015))
|
||||
- Zitadel: run init and setup in the foreground [@MickLesk](https://github.com/MickLesk) ([#18016](https://github.com/community-scripts/ProxmoxVE/pull/18016))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Zitadel: Support v1 and v2 Login [@MickLesk](https://github.com/MickLesk) ([#18031](https://github.com/community-scripts/ProxmoxVE/pull/18031))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: OpenWebUI (remove numba pin) [@MickLesk](https://github.com/MickLesk) ([#18026](https://github.com/community-scripts/ProxmoxVE/pull/18026))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Only treat an NVIDIA GPU as present when its device nodes are [@MickLesk](https://github.com/MickLesk) ([core#153](https://github.com/community-scripts/core/pull/153))
|
||||
|
||||
## 2026-10-19
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Calagopus ([#18012](https://github.com/community-scripts/ProxmoxVE/pull/18012))
|
||||
- Netronome ([#18011](https://github.com/community-scripts/ProxmoxVE/pull/18011))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- immich: record library revisions when the line is missing [@mazelado](https://github.com/mazelado) ([#17990](https://github.com/community-scripts/ProxmoxVE/pull/17990))
|
||||
- Update authentik 2026.8.4 [@thieneret](https://github.com/thieneret) ([#18004](https://github.com/community-scripts/ProxmoxVE/pull/18004))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- feat(mealie): add reverse proxy auth settings to mealie.env [@bferd](https://github.com/bferd) ([#18006](https://github.com/community-scripts/ProxmoxVE/pull/18006))
|
||||
|
||||
## 2026-10-18
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Donetick: create selfhosted.yaml from the example [@MickLesk](https://github.com/MickLesk) ([#17992](https://github.com/community-scripts/ProxmoxVE/pull/17992))
|
||||
- Wiki.js: only back up db.sqlite on SQLite installs [@MickLesk](https://github.com/MickLesk) ([#17993](https://github.com/community-scripts/ProxmoxVE/pull/17993))
|
||||
- FlareSolverr: pass Chrome an en-US Accept-Language [@MickLesk](https://github.com/MickLesk) ([#17995](https://github.com/community-scripts/ProxmoxVE/pull/17995))
|
||||
- 2FAuth: install the locked dependencies [@MickLesk](https://github.com/MickLesk) ([#17996](https://github.com/community-scripts/ProxmoxVE/pull/17996))
|
||||
- Tracearr: drop the pnpm store after the update build [@gioxx](https://github.com/gioxx) ([#17869](https://github.com/community-scripts/ProxmoxVE/pull/17869))
|
||||
- vaultwarden: build before stopping the service [@mazelado](https://github.com/mazelado) ([#17983](https://github.com/community-scripts/ProxmoxVE/pull/17983))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- fix: geopulse 2.0.0 upgrade support (nginx .mjs, pg18, jwt cleanup) [@CrazyWolf13](https://github.com/CrazyWolf13) ([#17978](https://github.com/community-scripts/ProxmoxVE/pull/17978))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Let get_current_ip reach its IPv6 fallback [@MickLesk](https://github.com/MickLesk) ([core#151](https://github.com/community-scripts/core/pull/151))
|
||||
- Drop apt autoclean before apt clean [@MickLesk](https://github.com/MickLesk) ([core#152](https://github.com/community-scripts/core/pull/152))
|
||||
- Silence apt's script warning in $STD [@MickLesk](https://github.com/MickLesk) ([core#150](https://github.com/community-scripts/core/pull/150))
|
||||
|
||||
## 2026-10-17
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- ProxCenter ([#17961](https://github.com/community-scripts/ProxmoxVE/pull/17961))
|
||||
- Listenarr ([#17963](https://github.com/community-scripts/ProxmoxVE/pull/17963))
|
||||
- Youtarr ([#17962](https://github.com/community-scripts/ProxmoxVE/pull/17962))
|
||||
- Norish ([#17964](https://github.com/community-scripts/ProxmoxVE/pull/17964))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Sonarr: stay on the newest v4 (pin) [@MickLesk](https://github.com/MickLesk) ([#17958](https://github.com/community-scripts/ProxmoxVE/pull/17958))
|
||||
|
||||
## 2026-10-16
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Downtify ([#17959](https://github.com/community-scripts/ProxmoxVE/pull/17959))
|
||||
- openGym ([#17960](https://github.com/community-scripts/ProxmoxVE/pull/17960))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Pelican Panel: pass the URL to p:environment:setup [@MickLesk](https://github.com/MickLesk) ([#17948](https://github.com/community-scripts/ProxmoxVE/pull/17948))
|
||||
- OneDev: use the onedev.server unit of 16.9 [@MickLesk](https://github.com/MickLesk) ([#17949](https://github.com/community-scripts/ProxmoxVE/pull/17949))
|
||||
- ReadMeABook: start the scheduler with the service [@MickLesk](https://github.com/MickLesk) ([#17950](https://github.com/community-scripts/ProxmoxVE/pull/17950))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Clean up after installs and updates again [@MickLesk](https://github.com/MickLesk) ([core#147](https://github.com/community-scripts/core/pull/147))
|
||||
- Fix apt_conffile_report exiting 1 when uutils comm sees locale-sorted input [@BadFlo](https://github.com/BadFlo) ([core#146](https://github.com/community-scripts/core/pull/146))
|
||||
|
||||
## 2026-10-15
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(apprise-api): copy static assets into the served directory [@brokentokin](https://github.com/brokentokin) ([#17940](https://github.com/community-scripts/ProxmoxVE/pull/17940))
|
||||
- Bytestash: modify backup process | Update Resources [@MickLesk](https://github.com/MickLesk) ([#17935](https://github.com/community-scripts/ProxmoxVE/pull/17935))
|
||||
- Zabbix: Add '-y' flag to apt install commands [@MickLesk](https://github.com/MickLesk) ([#17933](https://github.com/community-scripts/ProxmoxVE/pull/17933))
|
||||
- karakeep: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17087](https://github.com/community-scripts/ProxmoxVE/pull/17087))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep VM release lookups working without system.func [@MickLesk](https://github.com/MickLesk) ([core#144](https://github.com/community-scripts/core/pull/144))
|
||||
- Stop calling exit 1 "Operation not permitted" at several steps [@MickLesk](https://github.com/MickLesk) ([core#140](https://github.com/community-scripts/core/pull/140))
|
||||
|
||||
## 2026-10-14
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Paperclip: fall back to a non-frozen install when the release lockfile is stale [@MickLesk](https://github.com/MickLesk) ([#17922](https://github.com/community-scripts/ProxmoxVE/pull/17922))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Immich: rebuild the image libraries when the revision record is missing [@MickLesk](https://github.com/MickLesk) ([#17919](https://github.com/community-scripts/ProxmoxVE/pull/17919))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Show what the failing command printed, and keep verbose output in the log [@MickLesk](https://github.com/MickLesk) ([core#139](https://github.com/community-scripts/core/pull/139))
|
||||
- Install jq before the release lookup and show what a non-JSON answer was [@MickLesk](https://github.com/MickLesk) ([core#138](https://github.com/community-scripts/core/pull/138))
|
||||
- Refresh the NodeSource key before a same-major Node.js update [@MickLesk](https://github.com/MickLesk) ([core#137](https://github.com/community-scripts/core/pull/137))
|
||||
- github: say why a forge could not be reached instead of 'connection failed' [@MickLesk](https://github.com/MickLesk) ([core#136](https://github.com/community-scripts/core/pull/136))
|
||||
|
||||
## 2026-10-13
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fix URL for downloading OneDev latest release [@ryansully](https://github.com/ryansully) ([#17915](https://github.com/community-scripts/ProxmoxVE/pull/17915))
|
||||
- LocalAI: pin the CPU backends when the container has no GPU runtime [@MickLesk](https://github.com/MickLesk) ([#17909](https://github.com/community-scripts/ProxmoxVE/pull/17909))
|
||||
- [Gluetun] Update GitHub repository source install and update. [@CerberusStyle](https://github.com/CerberusStyle) ([#17899](https://github.com/community-scripts/ProxmoxVE/pull/17899))
|
||||
- Immich: Bump to 3.3.1 and restore (failed) geodata/countryInfo.txt on update [@MickLesk](https://github.com/MickLesk) ([#17894](https://github.com/community-scripts/ProxmoxVE/pull/17894))
|
||||
- WikiJS: use v2 Repo until v3 is stable [@MickLesk](https://github.com/MickLesk) ([#17895](https://github.com/community-scripts/ProxmoxVE/pull/17895))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- zammad: Debian 13, new apt repo, Elasticsearch 8 [@durzo](https://github.com/durzo) ([#17836](https://github.com/community-scripts/ProxmoxVE/pull/17836))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep the version key parseable by jq 1.6 [@MickLesk](https://github.com/MickLesk) ([core#134](https://github.com/community-scripts/core/pull/134))
|
||||
|
||||
## 2026-10-12
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- WatchState ([#17881](https://github.com/community-scripts/ProxmoxVE/pull/17881))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- chore(ct): sync wazuh defaults with PocketBase [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17889](https://github.com/community-scripts/ProxmoxVE/pull/17889))
|
||||
- Wanderer: bring back the wanderer-pb wrapper [@MickLesk](https://github.com/MickLesk) ([#17874](https://github.com/community-scripts/ProxmoxVE/pull/17874))
|
||||
- Tracearr: install only the server's production dependencies [@MickLesk](https://github.com/MickLesk) ([#17873](https://github.com/community-scripts/ProxmoxVE/pull/17873))
|
||||
- Paperless-ngx: QoL restart crashed services | patch regex [@MickLesk](https://github.com/MickLesk) ([#17812](https://github.com/community-scripts/ProxmoxVE/pull/17812))
|
||||
- Splunk Enterprise: start the service as the splunk user [@rcastley](https://github.com/rcastley) ([#17816](https://github.com/community-scripts/ProxmoxVE/pull/17816))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Draw a Community Scripts banner while a script has no header of its own [@MickLesk](https://github.com/MickLesk) ([core#133](https://github.com/community-scripts/core/pull/133))
|
||||
- Order pre-release labels when comparing versions [@MickLesk](https://github.com/MickLesk) ([core#132](https://github.com/community-scripts/core/pull/132))
|
||||
- gh: curl_download: wait between attempts [@MickLesk](https://github.com/MickLesk) ([core#131](https://github.com/community-scripts/core/pull/131))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich Public Proxy: optional visitor upload service (v4 Release) [@MickLesk](https://github.com/MickLesk) ([#17875](https://github.com/community-scripts/ProxmoxVE/pull/17875))
|
||||
|
||||
## 2026-10-11
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Cinephage ([#17852](https://github.com/community-scripts/ProxmoxVE/pull/17852))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Immich: build libvips with JPEG 2000 support [@MickLesk](https://github.com/MickLesk) ([#17864](https://github.com/community-scripts/ProxmoxVE/pull/17864))
|
||||
- hermesagent: match the relaunched root gateway regardless of interpreter [@MickLesk](https://github.com/MickLesk) ([#17847](https://github.com/community-scripts/ProxmoxVE/pull/17847))
|
||||
- Radicale: install the bcrypt and argon2 extras [@MickLesk](https://github.com/MickLesk) ([#17813](https://github.com/community-scripts/ProxmoxVE/pull/17813))
|
||||
- fix(zigbee2mqtt): preserve backup stream and prevent filename collisions [@petermnt](https://github.com/petermnt) ([#17751](https://github.com/community-scripts/ProxmoxVE/pull/17751))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- QoL: let var_* answer the optional add-on prompts [@MickLesk](https://github.com/MickLesk) ([#17858](https://github.com/community-scripts/ProxmoxVE/pull/17858))
|
||||
- QoL: Confirm third-party installers through confirm_external_installer [@MickLesk](https://github.com/MickLesk) ([#17857](https://github.com/community-scripts/ProxmoxVE/pull/17857))
|
||||
- docker: take the lxcfs toggle, TCP socket and Compose choice from app variables [@MickLesk](https://github.com/MickLesk) ([#17850](https://github.com/community-scripts/ProxmoxVE/pull/17850))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- RustDesk Server: run the official server, make the web UI optional [@MickLesk](https://github.com/MickLesk) ([#17854](https://github.com/community-scripts/ProxmoxVE/pull/17854))
|
||||
- Caddy: install the .deb from GitHub instead of the Cloudsmith repo [@MickLesk](https://github.com/MickLesk) ([#17862](https://github.com/community-scripts/ProxmoxVE/pull/17862))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Add confirm_external_installer [@MickLesk](https://github.com/MickLesk) ([core#130](https://github.com/community-scripts/core/pull/130))
|
||||
- setup_docker: make LXC resource limits visible to nested containers [@andrebrait](https://github.com/andrebrait) ([core#9](https://github.com/community-scripts/core/pull/9))
|
||||
- Incus: fix ENABLE_FUSE / TUN vars [@MickLesk](https://github.com/MickLesk) ([core#127](https://github.com/community-scripts/core/pull/127))
|
||||
- Resolve a uv required-version range to the newest release inside it [@MickLesk](https://github.com/MickLesk) ([core#126](https://github.com/community-scripts/core/pull/126))
|
||||
- Upgrade MongoDB in place when an app package depends on it [@MickLesk](https://github.com/MickLesk) ([core#128](https://github.com/community-scripts/core/pull/128))
|
||||
|
||||
## 2026-10-10
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Weblate ([#17837](https://github.com/community-scripts/ProxmoxVE/pull/17837))
|
||||
- CertMate ([#17803](https://github.com/community-scripts/ProxmoxVE/pull/17803))
|
||||
- AnythingLLM ([#17802](https://github.com/community-scripts/ProxmoxVE/pull/17802))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- update endurain repo from codeberg to gh [@johanngrobe](https://github.com/johanngrobe) ([#17831](https://github.com/community-scripts/ProxmoxVE/pull/17831))
|
||||
- Immich: download countryInfo.txt into the geodata directory on update [@claytonfaria](https://github.com/claytonfaria) ([#17823](https://github.com/community-scripts/ProxmoxVE/pull/17823))
|
||||
- paperclip: keep root and skip /opt/paperclip-data chown when PAPERCLIP_HOME is custom [@austinpilz](https://github.com/austinpilz) ([#17825](https://github.com/community-scripts/ProxmoxVE/pull/17825))
|
||||
|
||||
## 2026-10-09
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- FoldingAtHome ([#17799](https://github.com/community-scripts/ProxmoxVE/pull/17799))
|
||||
- LocalAI ([#17801](https://github.com/community-scripts/ProxmoxVE/pull/17801))
|
||||
- Tvheadend ([#17800](https://github.com/community-scripts/ProxmoxVE/pull/17800))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- OpenCloud: fix Collabora embedding with Collabora 26.04.4 [@SimKaiLong](https://github.com/SimKaiLong) ([#17810](https://github.com/community-scripts/ProxmoxVE/pull/17810))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Immich: survive an interrupted update and a failing ML build [@MickLesk](https://github.com/MickLesk) ([#17798](https://github.com/community-scripts/ProxmoxVE/pull/17798))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- check_for_release: compare against the release marked as latest [@MickLesk](https://github.com/MickLesk) ([core#125](https://github.com/community-scripts/core/pull/125))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- FileBrowser Quantum: migrate the config and database to v2 [@MickLesk](https://github.com/MickLesk) ([#17815](https://github.com/community-scripts/ProxmoxVE/pull/17815))
|
||||
|
||||
## 2026-10-08
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Unifi-OS-Server VM ([#17752](https://github.com/community-scripts/ProxmoxVE/pull/17752))
|
||||
- ZimaOS VM ([#17778](https://github.com/community-scripts/ProxmoxVE/pull/17778))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Revert "Immich: Pin to v3.3.0" [@MickLesk](https://github.com/MickLesk) ([#17767](https://github.com/community-scripts/ProxmoxVE/pull/17767))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Homarr: replace the musl better-sqlite3 that v2.3.0 ships for Debian [@MickLesk](https://github.com/MickLesk) ([#17789](https://github.com/community-scripts/ProxmoxVE/pull/17789))
|
||||
- CLIProxyAPI: keep plugins and data across updates [@MickLesk](https://github.com/MickLesk) ([#17790](https://github.com/community-scripts/ProxmoxVE/pull/17790))
|
||||
- Kima-Hub: install the Python services into a uv venv [@MickLesk](https://github.com/MickLesk) ([#17791](https://github.com/community-scripts/ProxmoxVE/pull/17791))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich: Pin to 3.3.0 / Update ML Python to 3.13 [@MickLesk](https://github.com/MickLesk) ([#17770](https://github.com/community-scripts/ProxmoxVE/pull/17770))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: OPNsense VM [@MickLesk](https://github.com/MickLesk) ([#17785](https://github.com/community-scripts/ProxmoxVE/pull/17785))
|
||||
- Refactor: Nextcloud VM (Turnkey) [@MickLesk](https://github.com/MickLesk) ([#17787](https://github.com/community-scripts/ProxmoxVE/pull/17787))
|
||||
- Refactor: OpenWrt VM [@MickLesk](https://github.com/MickLesk) ([#17774](https://github.com/community-scripts/ProxmoxVE/pull/17774))
|
||||
- Refactor: Ubuntu VM [@MickLesk](https://github.com/MickLesk) ([#17776](https://github.com/community-scripts/ProxmoxVE/pull/17776))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Tolerate a missing datacenter.cfg in vm_keyboard_default [@MickLesk](https://github.com/MickLesk) ([core#124](https://github.com/community-scripts/core/pull/124))
|
||||
- Take the ISO storage from the disk pool when it can hold ISOs [@MickLesk](https://github.com/MickLesk) ([core#122](https://github.com/community-scripts/core/pull/122))
|
||||
- Shared VM helpers: disk import, releases, checksums, first boot, IP by MAC [@MickLesk](https://github.com/MickLesk) ([core#117](https://github.com/community-scripts/core/pull/117))
|
||||
- tools: forge truncated release list [@MickLesk](https://github.com/MickLesk) ([core#118](https://github.com/community-scripts/core/pull/118))
|
||||
- Remember a kept cached image until the upstream changes [@MickLesk](https://github.com/MickLesk) ([core#119](https://github.com/community-scripts/core/pull/119))
|
||||
- Choose the VM keyboard layout and carry the host's timezone into prepared images [@MickLesk](https://github.com/MickLesk) ([core#120](https://github.com/community-scripts/core/pull/120))
|
||||
- VM's: run first-boot units without debconf dialogs [@MickLesk](https://github.com/MickLesk) ([core#121](https://github.com/community-scripts/core/pull/121))
|
||||
|
||||
## 2026-10-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Fedora VM ([#17747](https://github.com/community-scripts/ProxmoxVE/pull/17747))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- AudioMuse-AI: replace the venv on update without asking [@MickLesk](https://github.com/MickLesk) ([#17738](https://github.com/community-scripts/ProxmoxVE/pull/17738))
|
||||
- paperclip: run as a dedicated non-root user so Claude Code can skip permissions [@samvandenbossche](https://github.com/samvandenbossche) ([#17707](https://github.com/community-scripts/ProxmoxVE/pull/17707))
|
||||
- Thingsboard: update Java version from 17 to 25 [@CerberusStyle](https://github.com/CerberusStyle) ([#17733](https://github.com/community-scripts/ProxmoxVE/pull/17733))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Immich: Pin to v3.3.0 [@vhsdream](https://github.com/vhsdream) ([#17759](https://github.com/community-scripts/ProxmoxVE/pull/17759))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- OpenCloud: bump to v8.1.0, rebuild search index on upgrade [@SimKaiLong](https://github.com/SimKaiLong) ([#17710](https://github.com/community-scripts/ProxmoxVE/pull/17710))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Pangolin: Unpin Release, stable enough, Bump to latest [@MickLesk](https://github.com/MickLesk) ([#17740](https://github.com/community-scripts/ProxmoxVE/pull/17740))
|
||||
- Refactor: Archlinux-VM [@MickLesk](https://github.com/MickLesk) ([#17741](https://github.com/community-scripts/ProxmoxVE/pull/17741))
|
||||
- Refactor: MikroTik RouterOS [@MickLesk](https://github.com/MickLesk) ([#17748](https://github.com/community-scripts/ProxmoxVE/pull/17748))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Fix Fedora and BLS cloud image console setup [@MickLesk](https://github.com/MickLesk) ([core#116](https://github.com/community-scripts/core/pull/116))
|
||||
- Keep setup_nodejs alive when the caller's directory is gone [@MickLesk](https://github.com/MickLesk) ([core#114](https://github.com/community-scripts/core/pull/114))
|
||||
|
||||
## 2026-10-06
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Pricebuddy ([#17708](https://github.com/community-scripts/ProxmoxVE/pull/17708))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Webtrees: stop serving data/ and the source folders directly [@MickLesk](https://github.com/MickLesk) ([#17724](https://github.com/community-scripts/ProxmoxVE/pull/17724))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Incus: check the architecture on Incus hosts too [@MickLesk](https://github.com/MickLesk) ([core#113](https://github.com/community-scripts/core/pull/113))
|
||||
- Incus: Pass the app's var_* settings into Incus containers [@MickLesk](https://github.com/MickLesk) ([core#112](https://github.com/community-scripts/core/pull/112))
|
||||
- Incus: stub storage_content_check on Incus [@MickLesk](https://github.com/MickLesk) ([core#104](https://github.com/community-scripts/core/pull/104))
|
||||
- Incus: map Proxmox template versions to Incus image names [@MickLesk](https://github.com/MickLesk) ([core#111](https://github.com/community-scripts/core/pull/111))
|
||||
- Incus: read storage names from .vars files [@MickLesk](https://github.com/MickLesk) ([core#110](https://github.com/community-scripts/core/pull/110))
|
||||
- Incus: show the spinner inside containers again [@MickLesk](https://github.com/MickLesk) ([core#109](https://github.com/community-scripts/core/pull/109))
|
||||
- Incus: set the hostname with sh, so it works before bash is installed [@MickLesk](https://github.com/MickLesk) ([core#102](https://github.com/community-scripts/core/pull/102))
|
||||
- Incus: reserve a plain address on Incus, leave the gateway to the network [@MickLesk](https://github.com/MickLesk) ([core#101](https://github.com/community-scripts/core/pull/101))
|
||||
- Incus: Wait for the network on Alpine OS too [@MickLesk](https://github.com/MickLesk) ([core#105](https://github.com/community-scripts/core/pull/105))
|
||||
- Incus: do not fail the build when hostname -I is missing [@MickLesk](https://github.com/MickLesk) ([core#103](https://github.com/community-scripts/core/pull/103))
|
||||
- Incus: Fuse always in unprivileged Containers, attach TUN only when container lacks it [@MickLesk](https://github.com/MickLesk) ([core#108](https://github.com/community-scripts/core/pull/108))
|
||||
- Incus: accept mode=generated [@MickLesk](https://github.com/MickLesk) ([core#106](https://github.com/community-scripts/core/pull/106))
|
||||
- Incus: pass the full install environment into Incus containers (quoted) [@MickLesk](https://github.com/MickLesk) ([core#107](https://github.com/community-scripts/core/pull/107))
|
||||
|
||||
## 2026-10-05
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Point to DevScripts, the renamed ProxmoxVED [@MickLesk](https://github.com/MickLesk) ([#17694](https://github.com/community-scripts/ProxmoxVE/pull/17694))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- wanderer: set the proxy secret and install plugins in their own directories [@MickLesk](https://github.com/MickLesk) ([#17698](https://github.com/community-scripts/ProxmoxVE/pull/17698))
|
||||
- discourse: serve stylesheets and repair the update [@MickLesk](https://github.com/MickLesk) ([#17697](https://github.com/community-scripts/ProxmoxVE/pull/17697))
|
||||
- wikijs: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17689](https://github.com/community-scripts/ProxmoxVE/pull/17689))
|
||||
- jotty: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17688](https://github.com/community-scripts/ProxmoxVE/pull/17688))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Read releases from github.com when the API is rate limited, resume stalled downloads [@MickLesk](https://github.com/MickLesk) ([core#99](https://github.com/community-scripts/core/pull/99))
|
||||
- Check for template and container storage before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#98](https://github.com/community-scripts/core/pull/98))
|
||||
- Check /etc/pve before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#97](https://github.com/community-scripts/core/pull/97))
|
||||
- Check container settings before pct create rejects them [@MickLesk](https://github.com/MickLesk) ([core#96](https://github.com/community-scripts/core/pull/96))
|
||||
- PVE: Smart Diagnosis - say why a container would not start [@MickLesk](https://github.com/MickLesk) ([core#95](https://github.com/community-scripts/core/pull/95))
|
||||
- Retry template downloads that pveam reports as done but are not [@MickLesk](https://github.com/MickLesk) ([core#94](https://github.com/community-scripts/core/pull/94))
|
||||
- Check the OS release against pve-container before creating anything [@MickLesk](https://github.com/MickLesk) ([core#93](https://github.com/community-scripts/core/pull/93))
|
||||
- Rename "ProxmoxVED"-Links to "DevScripts" [@MickLesk](https://github.com/MickLesk) ([core#91](https://github.com/community-scripts/core/pull/91))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- monitor-all: read container IPs from the host side [@jasonobrien](https://github.com/jasonobrien) ([#17686](https://github.com/community-scripts/ProxmoxVE/pull/17686))
|
||||
|
||||
## 2026-10-04
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- immich: read the Intel runtime from the pinned release [@MickLesk](https://github.com/MickLesk) ([#17677](https://github.com/community-scripts/ProxmoxVE/pull/17677))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- better explain unsupported version & upgrade path for pve [@MickLesk](https://github.com/MickLesk) ([core#86](https://github.com/community-scripts/core/pull/86))
|
||||
- Quote PostgreSQL identifiers and drop spaces from the creds file name [@MickLesk](https://github.com/MickLesk) ([core#89](https://github.com/community-scripts/core/pull/89))
|
||||
- Find the default Rust toolchain in current rustup output [@MickLesk](https://github.com/MickLesk) ([core#88](https://github.com/community-scripts/core/pull/88))
|
||||
|
||||
## 2026-10-03
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
## 2026-11-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
|
||||
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
|
||||
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
|
||||
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
|
||||
|
||||
## 2026-11-06
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- UmbrelOS-VM: report runs as ProxmoxVE, not DevScripts [@MickLesk](https://github.com/MickLesk) ([#18497](https://github.com/community-scripts/ProxmoxVE/pull/18497))
|
||||
- Caddy: skip the xCaddy test build below 2 GB RAM [@MickLesk](https://github.com/MickLesk) ([#18472](https://github.com/community-scripts/ProxmoxVE/pull/18472))
|
||||
- Thingsboard: keep the package defaults in thingsboard.conf [@MickLesk](https://github.com/MickLesk) ([#18499](https://github.com/community-scripts/ProxmoxVE/pull/18499))
|
||||
- Nexterm: keep the extracted app out of the /tmp cleanup [@MickLesk](https://github.com/MickLesk) ([#18501](https://github.com/community-scripts/ProxmoxVE/pull/18501))
|
||||
- PLANKA: run the service with node instead of nodemon [@MickLesk](https://github.com/MickLesk) ([#18503](https://github.com/community-scripts/ProxmoxVE/pull/18503))
|
||||
- MeTube/Yubal: install Deno without its shell setup [@MickLesk](https://github.com/MickLesk) ([#18406](https://github.com/community-scripts/ProxmoxVE/pull/18406))
|
||||
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Technitium DNS: read the version from its GitHub releases [@MickLesk](https://github.com/MickLesk) ([#18425](https://github.com/community-scripts/ProxmoxVE/pull/18425))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
|
||||
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- ci: reject DevScripts URLs in scripts [@MickLesk](https://github.com/MickLesk) ([#18498](https://github.com/community-scripts/ProxmoxVE/pull/18498))
|
||||
|
||||
## 2026-11-05
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
|
||||
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
|
||||
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
|
||||
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
|
||||
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
|
||||
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
|
||||
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: keep the existing install until the new archive has extracted [@rcourtman](https://github.com/rcourtman) ([core#201](https://github.com/community-scripts/core/pull/201))
|
||||
- fix: keep the install when a clone or an Alpine extract fails [@rcourtman](https://github.com/rcourtman) ([core#210](https://github.com/community-scripts/core/pull/210))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Arcane: write both env names, stop shipping the unused .env [@MickLesk](https://github.com/MickLesk) ([#18466](https://github.com/community-scripts/ProxmoxVE/pull/18466))
|
||||
|
||||
## 2026-11-04
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Teleport: Poll readyz instead of fixed 10 second sleep. [@cthach](https://github.com/cthach) ([#18461](https://github.com/community-scripts/ProxmoxVE/pull/18461))
|
||||
- n8n: install past its unsatisfiable canvas dependencies [@MickLesk](https://github.com/MickLesk) ([#18439](https://github.com/community-scripts/ProxmoxVE/pull/18439))
|
||||
- Teable: add Redis cache [@MickLesk](https://github.com/MickLesk) ([#18450](https://github.com/community-scripts/ProxmoxVE/pull/18450))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: say why download_gpg_key failed [@MickLesk](https://github.com/MickLesk) ([core#208](https://github.com/community-scripts/core/pull/208))
|
||||
- feat: read the MOTD's OS version at login [@MickLesk](https://github.com/MickLesk) ([core#207](https://github.com/community-scripts/core/pull/207))
|
||||
- feat: skip screen clear when there is no terminal | pin only the scripts own app with var_appversion [@MickLesk](https://github.com/MickLesk) ([core#203](https://github.com/community-scripts/core/pull/203))
|
||||
|
||||
## 2026-11-03
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fedora VM: let the engine install libguestfs-tools [@MickLesk](https://github.com/MickLesk) ([#18422](https://github.com/community-scripts/ProxmoxVE/pull/18422))
|
||||
- Cinephage: run the camoufox binary the package now ships [@MickLesk](https://github.com/MickLesk) ([#18426](https://github.com/community-scripts/ProxmoxVE/pull/18426))
|
||||
- AlmaLinux VM: report failures through the engine's error handler [@MickLesk](https://github.com/MickLesk) ([#18418](https://github.com/community-scripts/ProxmoxVE/pull/18418))
|
||||
- WireGuard: give the Debian container 1 GB for WGDashboard [@MickLesk](https://github.com/MickLesk) ([#18419](https://github.com/community-scripts/ProxmoxVE/pull/18419))
|
||||
- zabbix: use os specific apt packages [@MickLesk](https://github.com/MickLesk) ([#18417](https://github.com/community-scripts/ProxmoxVE/pull/18417))
|
||||
- Discourse: build libvips 8.18 for its image worker [@MickLesk](https://github.com/MickLesk) ([#18430](https://github.com/community-scripts/ProxmoxVE/pull/18430))
|
||||
- Wazuh: show the installer's own error when it fails [@MickLesk](https://github.com/MickLesk) ([#18428](https://github.com/community-scripts/ProxmoxVE/pull/18428))
|
||||
- Tracearr: move to Node.js 26 [@connorgallopo](https://github.com/connorgallopo) ([#18393](https://github.com/community-scripts/ProxmoxVE/pull/18393))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Wiki.js: install 3.x [@MickLesk](https://github.com/MickLesk) ([#18382](https://github.com/community-scripts/ProxmoxVE/pull/18382))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- OpenWrt VM: read the stable release from downloads.openwrt.org [@MickLesk](https://github.com/MickLesk) ([#18411](https://github.com/community-scripts/ProxmoxVE/pull/18411))
|
||||
- Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first [@MickLesk](https://github.com/MickLesk) ([#18397](https://github.com/community-scripts/ProxmoxVE/pull/18397))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: put the reason for a VM failure into its trace [@MickLesk](https://github.com/MickLesk) ([core#200](https://github.com/community-scripts/core/pull/200))
|
||||
- fix: check the bridge before creating a container, and its state after start [@MickLesk](https://github.com/MickLesk) ([core#199](https://github.com/community-scripts/core/pull/199))
|
||||
- ref: offer to remove only the container this run created [@MickLesk](https://github.com/MickLesk) ([core#198](https://github.com/community-scripts/core/pull/198))
|
||||
- feat: report a cancelled tool dialog as an abort and name the failing command [@MickLesk](https://github.com/MickLesk) ([core#197](https://github.com/community-scripts/core/pull/197))
|
||||
- feat: check KVM and the host's threads before building a VM [@MickLesk](https://github.com/MickLesk) ([core#196](https://github.com/community-scripts/core/pull/196))
|
||||
- ref: give a host tool one log file for the whole run [@MickLesk](https://github.com/MickLesk) ([core#194](https://github.com/community-scripts/core/pull/194))
|
||||
- fix: keep a piped stdin when output moves to the log after a hangup [@MickLesk](https://github.com/MickLesk) ([core#193](https://github.com/community-scripts/core/pull/193))
|
||||
- fix: pve-LXC: say when the container went away during its setup [@MickLesk](https://github.com/MickLesk) ([core#192](https://github.com/community-scripts/core/pull/192))
|
||||
- fix: treat Esc in a VM wizard dialog as leaving the script [@MickLesk](https://github.com/MickLesk) ([core#190](https://github.com/community-scripts/core/pull/190))
|
||||
- ref: count a container stopped during a detached install as an abort [@MickLesk](https://github.com/MickLesk) ([core#187](https://github.com/community-scripts/core/pull/187))
|
||||
- feat: find the assets of a renamed GitHub repo without the API [@MickLesk](https://github.com/MickLesk) ([core#188](https://github.com/community-scripts/core/pull/188))
|
||||
- fix: restore npm when an interrupted update left it behind [@MickLesk](https://github.com/MickLesk) ([core#189](https://github.com/community-scripts/core/pull/189))
|
||||
- feat: add var_cmode to set the container console mode [@MickLesk](https://github.com/MickLesk) ([core#180](https://github.com/community-scripts/core/pull/180))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- all-templates: answering No or Cancel is not a failure [@MickLesk](https://github.com/MickLesk) ([#18423](https://github.com/community-scripts/ProxmoxVE/pull/18423))
|
||||
- homebrew: skip service accounts when picking the user, survive a failed apt update [@MickLesk](https://github.com/MickLesk) ([#18424](https://github.com/community-scripts/ProxmoxVE/pull/18424))
|
||||
- add-tailscale-lxc: start a stopped container and check its OS first [@MickLesk](https://github.com/MickLesk) ([#18421](https://github.com/community-scripts/ProxmoxVE/pull/18421))
|
||||
- netdata: survive a failed apt update, leave quietly on Cancel [@MickLesk](https://github.com/MickLesk) ([#18427](https://github.com/community-scripts/ProxmoxVE/pull/18427))
|
||||
- Privilege-Converter: offer only container-capable storages as the converter's restore target [@MickLesk](https://github.com/MickLesk) ([#18415](https://github.com/community-scripts/ProxmoxVE/pull/18415))
|
||||
- let post-pve-install go on when legacy sources are kept [@MickLesk](https://github.com/MickLesk) ([#18403](https://github.com/community-scripts/ProxmoxVE/pull/18403))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- update-apps: say why an backup or restore failed [@MickLesk](https://github.com/MickLesk) ([#18405](https://github.com/community-scripts/ProxmoxVE/pull/18405))
|
||||
- Keep host tools running when clear does not know the terminal [@MickLesk](https://github.com/MickLesk) ([#18412](https://github.com/community-scripts/ProxmoxVE/pull/18412))
|
||||
- Refuse to run the PBS 4 upgrade on a Proxmox VE host [@MickLesk](https://github.com/MickLesk) ([#18416](https://github.com/community-scripts/ProxmoxVE/pull/18416))
|
||||
- fstrim: keep trimming when pct fstrim fails on one container [@MickLesk](https://github.com/MickLesk) ([#18413](https://github.com/community-scripts/ProxmoxVE/pull/18413))
|
||||
- execute-lxc: do not abort on a container that is not Debian or Ubuntu [@MickLesk](https://github.com/MickLesk) ([#18414](https://github.com/community-scripts/ProxmoxVE/pull/18414))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- github: flag instead of closing on first PR template miss; add script-request eligibility check [@MickLesk](https://github.com/MickLesk) ([#18384](https://github.com/community-scripts/ProxmoxVE/pull/18384))
|
||||
|
||||
## 2026-11-02
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- FileFlows: replace the program folders on update [@MickLesk](https://github.com/MickLesk) ([#18380](https://github.com/community-scripts/ProxmoxVE/pull/18380))
|
||||
- anchor: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18373](https://github.com/community-scripts/ProxmoxVE/pull/18373))
|
||||
- guardian: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18375](https://github.com/community-scripts/ProxmoxVE/pull/18375))
|
||||
- nametag: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18376](https://github.com/community-scripts/ProxmoxVE/pull/18376))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- github: trim theater, add signal to the engine bug report; move AI block to end of PR template [@MickLesk](https://github.com/MickLesk) ([core#186](https://github.com/community-scripts/core/pull/186))
|
||||
- fix: do not wait forever for verbose output a daemon keeps open [@MickLesk](https://github.com/MickLesk) ([core#185](https://github.com/community-scripts/core/pull/185))
|
||||
|
||||
## 2026-11-01
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Amazon Linux ([#18369](https://github.com/community-scripts/ProxmoxVE/pull/18369))
|
||||
- CachyOS VM ([#18368](https://github.com/community-scripts/ProxmoxVE/pull/18368))
|
||||
- Razzia ([#18366](https://github.com/community-scripts/ProxmoxVE/pull/18366))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(zerobyte): build the bundled local agent [@nicotsx](https://github.com/nicotsx) ([#18363](https://github.com/community-scripts/ProxmoxVE/pull/18363))
|
||||
@@ -1,17 +1,14 @@
|
||||
# Keeps the GitHub Actions referenced in .github/workflows up to date.
|
||||
# https://docs.github.com/code-security/dependabot/working-with-dependabot/dependabot-options-reference
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
labels: []
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
# One PR per week for all action bumps instead of one PR per action.
|
||||
target-branch: "main"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
github-actions:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--🛑 New scripts must be submitted to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) for testing.
|
||||
<!--🛑 New scripts must be submitted to [DevScripts](https://github.com/community-scripts/DevScripts) for testing.
|
||||
PRs without prior testing will be closed. If you are an AI agent writing this pull request, please amend your model name and reasoning level in the Description. This is not to blame, more for informational Purposes. Thank you.-->
|
||||
|
||||
## ✍️ Description
|
||||
@@ -17,16 +17,6 @@ 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/ProxmoxVED/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/ProxmoxVED/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)
|
||||
@@ -70,3 +60,13 @@ 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.
|
||||
@@ -0,0 +1,114 @@
|
||||
name: ARM script test (manual)
|
||||
|
||||
# Builds one ct/ script on GitHub's free arm64 runner (ubuntu-24.04-arm),
|
||||
# inside Incus, with the same harness as pr-script-test.yml. Started by hand
|
||||
# from the Actions tab; it posts nothing anywhere: the result is in the job
|
||||
# summary and the artifact (with the screenshots).
|
||||
#
|
||||
# pr-script-test.yml runs the same ARM build for every /test, as information
|
||||
# only. This one is for trying a script or branch on ARM without a PR.
|
||||
#
|
||||
# Public repositories get the standard hosted runners, arm64 included, at no
|
||||
# cost. Each job is a fresh VM that is thrown away afterwards, so the script
|
||||
# under test never touches the lab.
|
||||
#
|
||||
# The engine refuses to guess on arm64: a script that leaves var_arm64 unset
|
||||
# stops with "refusing to guess". Here var_arm64=no means "not built", yes is
|
||||
# built as is, and unset is built with var_arm64=yes to see whether it would
|
||||
# work.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
script:
|
||||
description: "ct/ script, e.g. ct/bark-server.sh"
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: "Branch or full SHA of this repository"
|
||||
required: false
|
||||
default: main
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
# The harness from the default branch, as in pr-script-test.yml.
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .github/workflows/scripts
|
||||
persist-credentials: false
|
||||
|
||||
- name: Decide var_arm64
|
||||
id: plan
|
||||
env:
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
REF: ${{ inputs.ref }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
[[ "$SCRIPT" =~ ^ct/[A-Za-z0-9._-]+\.sh$ ]] || { echo "::error::script must be ct/<name>.sh"; exit 1; }
|
||||
[[ "$REF" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "::error::bad ref"; exit 1; }
|
||||
sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)
|
||||
text=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${SCRIPT}?ref=${sha}" --jq .content | base64 -d)
|
||||
# A commented-out line ("#var_arm64=...") counts as unset.
|
||||
arm=$(grep -m1 -oE '^[[:space:]]*var_arm64="[$][{]var_arm64:-(yes|no)[}]"' <<<"$text" | grep -oE ':-(yes|no)' | cut -c3- || true)
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
echo "arm64=${arm:-unset}" >>"$GITHUB_OUTPUT"
|
||||
case "$arm" in
|
||||
no) echo "::notice::${SCRIPT} sets var_arm64=no: not built on ARM" ;;
|
||||
yes) echo "${SCRIPT} sets var_arm64=yes" ;;
|
||||
*) echo "::notice::${SCRIPT} leaves var_arm64 unset: building with var_arm64=yes to see whether it works" ;;
|
||||
esac
|
||||
echo "Building ${SCRIPT} at ${sha:0:7} on $(uname -m), $(nproc) cores, $(free -g | awk '/^Mem/ {print $2}') GB RAM, $(df -h / | awk 'NR==2 {print $4}') free"
|
||||
|
||||
- name: Set up Incus
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
|
||||
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
|
||||
|
||||
- name: Build and check ${{ inputs.script }}
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
PLATFORM: incus
|
||||
VAR_ARM64: "yes"
|
||||
PR_REPO: ${{ github.repository }}
|
||||
PR_SHA: ${{ steps.plan.outputs.sha }}
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
UPSTREAM: ${{ github.repository }}
|
||||
CTID_BASE: "9000"
|
||||
GH_API_TOKEN: ${{ github.token }}
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
BUILD_TIMEOUT: 90m
|
||||
run: sudo --preserve-env=PLATFORM,VAR_ARM64,PR_REPO,PR_SHA,SCRIPT,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,BUILD_TIMEOUT,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT bash .github/workflows/scripts/pve-run-script.sh
|
||||
|
||||
- name: Summary
|
||||
if: always() && steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
ARM64: ${{ steps.plan.outputs.arm64 }}
|
||||
run: |
|
||||
sudo chmod -R a+rX "$OUT_DIR" 2>/dev/null || true
|
||||
r="$OUT_DIR/result.json"
|
||||
[[ -f "$r" ]] || { echo "No result.json" >>"$GITHUB_STEP_SUMMARY"; exit 0; }
|
||||
{
|
||||
echo "### ARM (arm64, Incus): $(jq -r .script "$r")"
|
||||
echo
|
||||
echo "| Status | Install | Update | Duration | var_arm64 | Detail |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
jq -r --arg arm "$ARM64" '"| \(.status) | \(if .update_status != "" then "pass" else .status end) | \(if .update_status == "" then "-" else .update_status end) | \(.duration)s | \($arm) | \(if .status == "pass" then (.url // "" | if . == "" then "no URL" else . + " → " + $ARGS.named.c end) else .step end) |"' --arg c "$(jq -r .http_code "$r")" "$r"
|
||||
echo
|
||||
ls "$OUT_DIR"/screenshot*.png &>/dev/null && echo "Screenshots: in the \`result-arm\` artifact." || echo "No screenshot."
|
||||
} >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: result-arm
|
||||
path: ${{ runner.temp }}/result
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
try {
|
||||
const { data: relatedIssues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: "ProxmoxVED",
|
||||
repo: "DevScripts",
|
||||
state: "all",
|
||||
labels: ["Started Migration To ProxmoxVE"]
|
||||
});
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Close PRs Missing Template
|
||||
name: Flag PRs Missing Template
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -14,10 +14,12 @@ jobs:
|
||||
issues: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Close PR if it does not follow the PR template
|
||||
- name: Check PR against the template, comment and label instead of closing
|
||||
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;
|
||||
@@ -44,11 +46,11 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = pr.labels.map((label) => label.name);
|
||||
const prLabels = pr.labels.map((label) => label.name);
|
||||
const skipLabels = ["automated pr", "keep-open"];
|
||||
|
||||
if (skipLabels.some((label) => labels.includes(label))) {
|
||||
core.info(`PR #${prNumber} has a skip label (${labels.join(", ")}) — skipping template validation.`);
|
||||
if (skipLabels.some((label) => prLabels.includes(label))) {
|
||||
core.info(`PR #${prNumber} has a skip label (${prLabels.join(", ")}) — skipping template validation.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,50 +116,52 @@ 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(`Closing PR #${prNumber} — template validation failed.`);
|
||||
core.info(`PR #${prNumber} — template validation failed, flagging instead of closing.`);
|
||||
|
||||
const templateUrl =
|
||||
"https://github.com/community-scripts/ProxmoxVE/blob/main/.github/pull_request_template.md";
|
||||
const failureList = failures.map((item) => `- ${item}`).join("\n");
|
||||
|
||||
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");
|
||||
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! 🙏`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: comment,
|
||||
});
|
||||
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.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: ["missing pr template"],
|
||||
});
|
||||
const toAdd = LABELS.filter(name => !prLabels.includes(name));
|
||||
if (toAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: toAdd });
|
||||
}
|
||||
@@ -86,10 +86,10 @@ jobs:
|
||||
``,
|
||||
`However, **new scripts must first be submitted to our development repository** for testing and review before they can be merged here.`,
|
||||
``,
|
||||
`> 🛑 New scripts must be submitted to [**ProxmoxVED**](https://github.com/community-scripts/ProxmoxVED) for testing.`,
|
||||
`> 🛑 New scripts must be submitted to [**DevScripts**](https://github.com/community-scripts/DevScripts) for testing.`,
|
||||
`> PRs without prior testing will be closed.`,
|
||||
``,
|
||||
`Please open your PR at **https://github.com/community-scripts/ProxmoxVED** instead.`,
|
||||
`Please open your PR at **https://github.com/community-scripts/DevScripts** instead.`,
|
||||
`Once your script has been tested and approved there, it will be pushed to this repository automatically.`,
|
||||
``,
|
||||
`This PR will now be closed. Thank you for understanding! 🙏`,
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
if: github.event.pull_request.merged == true && github.repository == 'community-scripts/ProxmoxVE'
|
||||
runs-on: self-hosted
|
||||
env:
|
||||
DEV_REPO: community-scripts/ProxmoxVED
|
||||
DEV_REPO: community-scripts/DevScripts
|
||||
|
||||
steps:
|
||||
- name: Checkout target repo (merge commit)
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
echo "count=$(printf '%s' "$slugs" | wc -w | tr -d '[:space:]')" >> "$GITHUB_OUTPUT"
|
||||
echo "Slugs in this PR: ${slugs:-<none>}"
|
||||
|
||||
- name: Resolve the matching ProxmoxVED issue
|
||||
- name: Resolve the matching DevScripts issue
|
||||
id: find_issue
|
||||
shell: bash
|
||||
env:
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
# creates, so migration PRs carry an exact back-reference. Guessing from
|
||||
# titles or file names is never needed for them.
|
||||
matched=$(printf '%s' "$PR_BODY" \
|
||||
| grep -oE 'community-scripts/ProxmoxVED#[0-9]+' \
|
||||
| grep -oE 'community-scripts/(ProxmoxVED|DevScripts)#[0-9]+' \
|
||||
| sed 's/.*#//' || true)
|
||||
|
||||
if [ -n "$matched" ]; then
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
[ -n "$row" ] || continue
|
||||
num=$(printf '%s' "$row" | jq -r '.number')
|
||||
|
||||
# Same slug derivation the ProxmoxVED workflows use, so both
|
||||
# Same slug derivation the DevScripts workflows use, so both
|
||||
# sides agree on what "alpine-cinny" means.
|
||||
name=$(printf '%s' "$row" | jq -r '.body // ""' \
|
||||
| sed -n '/^###[[:space:]]*Name of the Script/,/^###/p' \
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
echo "issue_numbers=$matched" >> "$GITHUB_OUTPUT"
|
||||
echo "Closing: ${matched:-<none>}"
|
||||
|
||||
- name: Comment on and close matching ProxmoxVED issues
|
||||
- name: Comment on and close matching DevScripts issues
|
||||
if: steps.find_issue.outputs.issue_numbers != ''
|
||||
shell: bash
|
||||
env:
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
|
||||
for (const slug of slugs) {
|
||||
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1&expand=type', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
const list = JSON.parse(listRes.body);
|
||||
@@ -239,14 +239,21 @@ jobs:
|
||||
console.log('Slug not in DB, skipping: ' + slug);
|
||||
continue;
|
||||
}
|
||||
const patch = {
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
};
|
||||
// Containers in ProxmoxVE are synced to Incus from here on.
|
||||
const recordType = record.expand && record.expand.type && record.expand.type.type;
|
||||
const platforms = Array.isArray(record.platforms) ? record.platforms : [];
|
||||
if (recordType === 'lxc' && !platforms.includes('incus')) patch.platforms = ['pve', 'incus'];
|
||||
else if (!platforms.length) patch.platforms = ['pve'];
|
||||
if (!Array.isArray(record.architectures) || !record.architectures.length) patch.architectures = ['amd64'];
|
||||
const patchRes = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
})
|
||||
body: JSON.stringify(patch)
|
||||
});
|
||||
if (!patchRes.ok) {
|
||||
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
|
||||
@@ -258,3 +265,20 @@ jobs:
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
|
||||
- name: Move the scripts' telemetry to ProxmoxVE
|
||||
if: ${{ !cancelled() && steps.find_issue.outputs.issue_numbers != '' && steps.get_slugs.outputs.count != '0' }}
|
||||
shell: bash
|
||||
env:
|
||||
TELEMETRY_ADMIN_TOKEN: ${{ secrets.TELEMETRY_ADMIN_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$TELEMETRY_ADMIN_TOKEN" ]; then
|
||||
echo "TELEMETRY_ADMIN_TOKEN is not set, skipping"
|
||||
exit 0
|
||||
fi
|
||||
body=$(jq -cn --arg s "$(cat pocketbase_slugs.txt)" \
|
||||
'{slugs: ($s | split(" ") | map(select(. != ""))), from: "DevScripts", to: "ProxmoxVE"}')
|
||||
curl -fsS -X POST "https://telemetry.community-scripts.org/api/admin/migrate-script" \
|
||||
-H "Authorization: Bearer ${TELEMETRY_ADMIN_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" || echo "::warning::Moving the telemetry history to ProxmoxVE failed"
|
||||
@@ -267,15 +267,14 @@ jobs:
|
||||
const stubPath = path.join('ct', slug + '.sh');
|
||||
const content = [
|
||||
"#!/usr/bin/env bash",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/core/main/core/core.func)",
|
||||
"# Copyright (c) 2021-2026 community-scripts ORG",
|
||||
"# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE",
|
||||
"",
|
||||
`APP="${appName.replace(/"/g, '\\"')}"`,
|
||||
"",
|
||||
'header_info "$APP"',
|
||||
'variables',
|
||||
'color',
|
||||
'header_info',
|
||||
"",
|
||||
'msg_error "This script is no longer available in community-scripts."',
|
||||
`msg_error "${deletedMessage.replace(/"/g, '\\"')}"`,
|
||||
|
||||
@@ -343,6 +343,7 @@ jobs:
|
||||
'`cpu` `ram` `hdd` `os` `version` also work without `method` — they go to the\n' +
|
||||
'default install method, e.g. `/pocketbase <slug> hdd=25`\n\n' +
|
||||
'**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `repository` ' +
|
||||
'`oidc_url` `license` ' +
|
||||
'`config_path` `port` `default_user` `default_passwd` ' +
|
||||
'`updateable` `privileged` `is_dev` `has_arm` ' +
|
||||
'`architectures` (amd64,arm64) `platforms` (pve,incus) ' +
|
||||
@@ -544,6 +545,7 @@ jobs:
|
||||
out.push('- **Port:** ' + (record.port != null ? '`' + record.port + '`' : '—'));
|
||||
out.push('- **Updateable:** ' + (record.updateable ? 'Yes' : 'No'));
|
||||
out.push('- **Privileged:** ' + (record.privileged ? 'Yes' : 'No'));
|
||||
out.push('- **License:** ' + (record.license || '—'));
|
||||
out.push('- **Architectures:** ' + ((record.architectures || []).join(', ') || 'amd64'));
|
||||
out.push('- **Platforms:** ' + ((record.platforms || []).join(', ') || 'pve'));
|
||||
if (record.is_dev) out.push('- **Dev:** Yes');
|
||||
@@ -554,6 +556,7 @@ jobs:
|
||||
out.push('**Links:**');
|
||||
out.push('- **Website:** ' + (record.website || '—'));
|
||||
out.push('- **Docs:** ' + (record.documentation || '—'));
|
||||
out.push('- **OIDC docs:** ' + (record.oidc_url || '—'));
|
||||
out.push('- **Logo:** ' + (record.logo ? '[link](' + record.logo + ')' : '—'));
|
||||
out.push('- **GitHub:** ' + (record.github || '—'));
|
||||
if (record.config_path) out.push('- **Config:** `' + record.config_path + '`');
|
||||
@@ -985,6 +988,8 @@ jobs:
|
||||
logo: 'string',
|
||||
documentation: 'string',
|
||||
website: 'string',
|
||||
oidc_url: 'string',
|
||||
license: 'string',
|
||||
project_url: 'string',
|
||||
repository: 'string',
|
||||
config_path: 'string',
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
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}`);
|
||||
}
|
||||
@@ -184,8 +184,8 @@ jobs:
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
'Only the script itself comes from this branch. Whatever it loads, the engine',
|
||||
'or the `misc/` helpers, still comes from `main`.',
|
||||
'Only the script itself comes from this branch. The engine it loads still',
|
||||
'comes from community-scripts/core on `main`.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turns a GitHub-hosted arm64 runner (ubuntu-24.04-arm) into a throwaway Incus
|
||||
# test host for pve-run-script.sh (PLATFORM=incus): the same layout as
|
||||
# incus-ci-host-setup.sh, minus what a VM that lives for one job does not need
|
||||
# (services, sweeper, runner registration). Run with sudo.
|
||||
#
|
||||
# The runner is a fresh VM per job and is discarded afterwards; nothing here
|
||||
# has to be undone.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NET_ADDR=10.98.0.1
|
||||
NET_CIDR=10.98.0.0/24
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
start=$(date +%s)
|
||||
|
||||
# --- Incus ----------------------------------------------------------------------
|
||||
# Ubuntu 24.04's own incus is 6.0.0, which lacks `-f csv,noheader`; the engine
|
||||
# then finds no storage pool. Zabbly (the Incus maintainers' packages) has the
|
||||
# current 6.0 LTS, as Debian 13 does.
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
|
||||
# The key Zabbly publishes in its install instructions, not whatever the
|
||||
# server returns today.
|
||||
gpg --show-keys --with-colons /etc/apt/keyrings/zabbly.asc 2>/dev/null |
|
||||
awk -F: '/^fpr/ {print $10; exit}' | grep -qx 4EFC590696CB15B87C73A3AD82CC8797C838DCFD ||
|
||||
{ echo "::error::unexpected Zabbly signing key"; exit 1; }
|
||||
# Only the Incus packages may come from there; everything else stays Ubuntu's.
|
||||
printf '%s\n' 'Package: *' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 1' '' \
|
||||
'Package: incus* liblxc* lxcfs* qemu* edk2*' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 600' \
|
||||
>/etc/apt/preferences.d/zabbly
|
||||
printf '%s\n' 'Enabled: yes' 'Types: deb' 'URIs: https://pkgs.zabbly.com/incus/lts-6.0' \
|
||||
"Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")" 'Components: main' \
|
||||
"Architectures: $(dpkg --print-architecture)" 'Signed-By: /etc/apt/keyrings/zabbly.asc' \
|
||||
>/etc/apt/sources.list.d/zabbly-incus-lts-6.0.sources
|
||||
# Each slow step has its own limit, so a hang names itself in the log instead
|
||||
# of holding the PR report until the job times out.
|
||||
step() { local t=$1; shift; timeout "$t" "$@" || { echo "::error::'$*' failed or hung (limit ${t})"; return 1; }; }
|
||||
# What the runner's network looks like when apt can't get through.
|
||||
netdiag() {
|
||||
echo "::group::Network on this runner"
|
||||
resolvectl status 2>&1 | grep -E 'DNS Servers|Current DNS|resolv.conf mode' || cat /etc/resolv.conf
|
||||
local h
|
||||
for h in ports.ubuntu.com pkgs.zabbly.com github.com; do
|
||||
echo "$h -> $(timeout 10 getent ahostsv4 "$h" | awk '{print $1; exit}')"
|
||||
timeout 15 curl -s -o /dev/null -w " http %{http_code} in %{time_total}s\n" "http://$h/" || echo " no answer"
|
||||
done
|
||||
echo "::endgroup::"
|
||||
}
|
||||
# The runner's resolver (Azure's 168.63.129.16) stopped resolving the arm64
|
||||
# mirror on 2026-11-11 while every other name worked. Then the name is
|
||||
# looked up over DNS-over-HTTPS and pinned in /etc/hosts for this job.
|
||||
for h in ports.ubuntu.com; do
|
||||
timeout 10 getent ahostsv4 "$h" >/dev/null && continue
|
||||
ip=$(curl -fsS --max-time 10 -H 'accept: application/dns-json' "https://1.1.1.1/dns-query?name=$h&type=A" |
|
||||
jq -r '[.Answer[]? | select(.type == 1) | .data][0] // empty')
|
||||
if [[ "$ip" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
|
||||
echo "$ip $h" >>/etc/hosts
|
||||
echo "::warning::the runner's DNS can't resolve $h; using $ip from 1.1.1.1"
|
||||
fi
|
||||
done
|
||||
step 5m apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 update -qq || { netdiag; exit 1; }
|
||||
DEBIAN_FRONTEND=noninteractive step 10m apt-get install -y -qq incus jq >/dev/null
|
||||
|
||||
# Docker on the runner sets the FORWARD policy to DROP, which cuts the Incus
|
||||
# containers off from the internet.
|
||||
iptables -P FORWARD ACCEPT
|
||||
|
||||
step 2m incus admin init --preseed <<EOF
|
||||
networks:
|
||||
- name: incusbr0
|
||||
type: bridge
|
||||
config:
|
||||
ipv4.address: ${NET_ADDR}/24
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: none
|
||||
storage_pools:
|
||||
- name: default
|
||||
driver: dir
|
||||
profiles:
|
||||
- name: default
|
||||
devices:
|
||||
root:
|
||||
path: /
|
||||
pool: default
|
||||
type: disk
|
||||
eth0:
|
||||
name: eth0
|
||||
network: incusbr0
|
||||
type: nic
|
||||
EOF
|
||||
|
||||
# The PR tree server the harness expects (a service on the lab hosts).
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
nohup python3 -m http.server "$SERVE_PORT" --bind "$NET_ADDR" --directory "$SERVE_ROOT" >/tmp/ci-serve.log 2>&1 &
|
||||
echo "Incus $(incus --version) ready after $(($(date +%s) - start))s"
|
||||
|
||||
# --- Screenshots ----------------------------------------------------------------
|
||||
# Optional: without them the build is still tested. The harness takes one only
|
||||
# when `chromium` exists, the user ci-shot exists and the CI_SHOT chain keeps
|
||||
# that user off private addresses (here: the cloud metadata service at
|
||||
# 169.254.169.254 and the runner itself).
|
||||
shots() {
|
||||
useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
# Ubuntu's apt chromium is a snap wrapper; Playwright ships a plain arm64
|
||||
# build. Started directly with --screenshot, that build crashes on this
|
||||
# runner (sandbox or not), so `chromium` here is a stand-in that takes the
|
||||
# harness's arguments and drives the browser through Playwright instead.
|
||||
mkdir -p /opt/pw
|
||||
(cd /opt/pw && step 5m npm install --silent --no-audit --no-fund playwright-core@1.49.1 >/dev/null) || return 1
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers DEBIAN_FRONTEND=noninteractive \
|
||||
step 10m /opt/pw/node_modules/.bin/playwright-core install --with-deps chromium >/dev/null || return 1
|
||||
chmod -R a+rX /opt/pw /opt/pw-browsers
|
||||
cat >/opt/pw/shot.js <<'EOF'
|
||||
// node shot.js <url> <out.png> <width> <height>
|
||||
const { chromium } = require('/opt/pw/node_modules/playwright-core');
|
||||
(async () => {
|
||||
const [url, out, w, h] = process.argv.slice(2);
|
||||
const browser = await chromium.launch({ chromiumSandbox: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: Number(w), height: Number(h) }, ignoreHTTPSErrors: true });
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 30000 }).catch(() => {});
|
||||
await page.waitForTimeout(5000);
|
||||
await page.screenshot({ path: out });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(e => { console.error(e.message); process.exit(1); });
|
||||
EOF
|
||||
cat >/usr/local/bin/chromium <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stand-in for `chromium --headless ... --screenshot=<file> --window-size=W,H <url>`
|
||||
# on the ARM runner (see arm-runner-setup.sh). Other flags are ignored.
|
||||
url="" out="" w=1280 h=800
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--version) echo "Chromium (Playwright $(node -p "require('/opt/pw/node_modules/playwright-core/package.json').version"))"; exit 0 ;;
|
||||
--screenshot=*) out="${a#*=}" ;;
|
||||
--window-size=*) s="${a#*=}"; w="${s%,*}"; h="${s#*,}" ;;
|
||||
-*) ;;
|
||||
*) url="$a" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$url" && -n "$out" ]] || { echo "usage: chromium --screenshot=<file> <url>" >&2; exit 2; }
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers exec node /opt/pw/shot.js "$url" "$out" "$w" "$h"
|
||||
EOF
|
||||
chmod 755 /usr/local/bin/chromium
|
||||
# Ubuntu 24.04 forbids unprivileged user namespaces through AppArmor, and
|
||||
# Chromium's sandbox needs them. The sandbox stays on; the restriction goes,
|
||||
# on a VM that exists for this one job.
|
||||
sysctl -qw kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true
|
||||
|
||||
local uid fw net ns
|
||||
uid=$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
$fw -w -N CI_SHOT
|
||||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d "$NET_ADDR" -j REJECT
|
||||
iptables -w -A CI_SHOT -d "$NET_CIDR" -j ACCEPT
|
||||
for ns in 127.0.0.53 $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /run/systemd/resolve/resolv.conf 2>/dev/null); do
|
||||
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
|
||||
# The same call the harness makes, on a page that needs no network.
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
chown ci-shot "$dir"
|
||||
# HOME: runuser keeps root's, which ci-shot cannot write (the harness does the same).
|
||||
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
|
||||
--no-first-run --window-size=320,200 --user-data-dir="$dir/profile" \
|
||||
--screenshot="$dir/shot.png" 'data:text/html,<h1>ci</h1>' &>"$dir/chromium.log" || true
|
||||
if [[ ! -s "$dir/shot.png" ]]; then
|
||||
echo "Chromium could not take a test screenshot:"
|
||||
head -n 15 "$dir/chromium.log"
|
||||
echo "userns: $(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>&1)"
|
||||
rm -rf -- "$dir"
|
||||
return 1
|
||||
fi
|
||||
rm -rf -- "$dir"
|
||||
echo "$(chromium --version) ready for screenshots"
|
||||
}
|
||||
shots || echo "::warning::no screenshots on this ARM run (the build is still tested)"
|
||||
echo "Runner ready after $(($(date +%s) - start))s"
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,643 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,140 @@
|
||||
# shellcheck shell=bash
|
||||
# The vm/ half of pve-run-script.sh, sourced from it: same inputs, same
|
||||
# result.json, same live output. Only building and checking differ.
|
||||
#
|
||||
# A VM script runs entirely on this host (there is no install script inside
|
||||
# the guest to follow), unattended through the core engine's VM_UNATTENDED=1:
|
||||
# the script's defaults, with ID, name, bridge and storage set from here. The
|
||||
# guest then boots on vmbr1 like the containers do, so its address is whatever
|
||||
# this host's dnsmasq leased to its MAC; no guest agent needed.
|
||||
#
|
||||
# What passes: the script exits 0 and says "Completed successfully", the VM is
|
||||
# running and keeps running through the boot wait, and where the script prints
|
||||
# a URL built from the VM's address (http://${VM_IP}:9000) that URL answers 200.
|
||||
# An installer ISO or an image without DHCP gets no address; that is reported,
|
||||
# not failed. There is no update test: a VM has no `update`.
|
||||
|
||||
VM_BOOT_WAIT="${VM_BOOT_WAIT:-300}"
|
||||
VM_URL_WAIT="${VM_URL_WAIT:-600}"
|
||||
LEASES="${LEASES:-/var/lib/misc/dnsmasq.leases}"
|
||||
|
||||
# One VM build per host at a time: a VM takes 2-8 GB of RAM and a disk image,
|
||||
# where a container takes a fraction. Held until the job exits; the build is
|
||||
# started with this descriptor closed, so a QEMU process can never inherit it.
|
||||
vm_wait_turn() {
|
||||
exec 7>/run/ci-jobs/.vm-turn
|
||||
flock -n 7 && return 0
|
||||
echo "Another VM build is running on this host; waiting for it (one VM at a time)"
|
||||
flock -w 3600 7 || setup_error "waited an hour for this host's VM slot"
|
||||
}
|
||||
|
||||
vm_cleanup() {
|
||||
if qm status "$ctid" &>/dev/null; then
|
||||
qm stop "$ctid" --skiplock 1 --timeout 30 &>/dev/null || true
|
||||
qm destroy "$ctid" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null || true
|
||||
fi
|
||||
rm -f "/run/ci-jobs/$ctid" "/run/ci-jobs/console-${ctid}.png"
|
||||
# Cloud-Init network snippets the script wrote for this VM.
|
||||
[[ -f "$RAW_LOG.start" ]] &&
|
||||
find /var/lib/vz/snippets -maxdepth 1 -type f -name "*${ctid}*" -newer "$RAW_LOG.start" -delete 2>/dev/null
|
||||
true
|
||||
}
|
||||
|
||||
# The VM's screen, straight from QEMU. Pixels only, but they come from the
|
||||
# guest, so the same checks as for a browser screenshot apply.
|
||||
vm_console_shot() {
|
||||
local f="/run/ci-jobs/console-${ctid}.png"
|
||||
rm -f "$f"
|
||||
printf 'screendump %s -f png\n' "$f" | timeout 30 qm monitor "$ctid" &>/dev/null
|
||||
if [[ -s "$f" && $(stat -c %s "$f") -lt 3000000 ]] &&
|
||||
[[ "$(head -c 8 "$f" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
|
||||
cp "$f" "$OUT_DIR/screenshot.png"
|
||||
fi
|
||||
rm -f "$f"
|
||||
}
|
||||
|
||||
vm_running() {
|
||||
qm status "$ctid" 2>/dev/null | grep -q 'status: running'
|
||||
}
|
||||
|
||||
vm_build_and_check() {
|
||||
local hn mac pattern waited last
|
||||
hn="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-40)-${ctid}"
|
||||
echo "Building ${SCRIPT} as VM ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
|
||||
local vm_env=(
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOME=/root TERM=dumb LANG=C.UTF-8 DIAGNOSTICS=no
|
||||
VM_UNATTENDED=1 VM_VMID="$ctid" VM_HOSTNAME="$hn" VM_BRIDGE="$BRIDGE"
|
||||
VM_STORAGE=local VM_ISO_STORAGE=local VM_START=yes
|
||||
COMMUNITY_SCRIPTS_URL="$base"
|
||||
)
|
||||
# Release lookups (vm_latest_from_github and the like).
|
||||
[[ -n "${GH_API_TOKEN:-}" ]] && vm_env+=(GITHUB_TOKEN="$GH_API_TOKEN")
|
||||
touch "$RAW_LOG.start"
|
||||
|
||||
env -i "${vm_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
|
||||
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 7>&- &
|
||||
build_pid=$!
|
||||
start_streams
|
||||
wait "$build_pid"
|
||||
exit_code=$?
|
||||
build_pid=""
|
||||
stop_streams
|
||||
|
||||
((exit_code == 124 || exit_code == 137)) && fail "build timed out after ${BUILD_TIMEOUT}"
|
||||
if ((exit_code != 0)); then
|
||||
last=$(failure_line)
|
||||
step="script exited ${exit_code}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
# vm_finish ends on a msg_ok of the script's choosing ("VM created. ..."),
|
||||
# older scripts on "Completed successfully". exit_script and friends leave
|
||||
# with 0 too, but on a cross.
|
||||
last=$(strip_ansi <"$RAW_LOG" | tail -n 1 | cut -c1-200)
|
||||
if ! grep -qi 'completed successfully' "$RAW_LOG" && ! [[ "$last" =~ ^[[:space:]]*✔ ]]; then
|
||||
fail "script exited 0 but never reported success${last:+ (last line: ${last})}"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} is not running after the script finished"
|
||||
|
||||
# The address dnsmasq handed to this VM's MAC. The VM must stay up while it
|
||||
# boots: one that powers itself off or crashes is a failure.
|
||||
mac=$(qm config "$ctid" 2>/dev/null | sed -nE 's/^net0:[^=]*=([0-9A-Fa-f:]{17}).*/\1/p' | tr 'A-F' 'a-f')
|
||||
echo "Waiting up to ${VM_BOOT_WAIT}s for VM ${ctid} (${mac:-no NIC}) to boot and get an address"
|
||||
for ((waited = 0; waited < VM_BOOT_WAIT; waited += 5)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while booting"
|
||||
[[ -n "$mac" ]] && ct_ip=$(awk -v m="$mac" '$2 == m {print $3}' "$LEASES" 2>/dev/null | tail -n 1)
|
||||
[[ -n "$ct_ip" ]] && break
|
||||
sleep 5
|
||||
done
|
||||
[[ -n "$ct_ip" ]] && echo "VM ${ctid} got ${ct_ip}" || echo "VM ${ctid} got no DHCP lease within ${VM_BOOT_WAIT}s"
|
||||
|
||||
# Only a URL the script builds from the address it found; <VM-IP> in a
|
||||
# summary is a hint for later (after an installer ran), not a promise.
|
||||
# ${VM_IP}, $VM_IP and ${VM_IP:-<VM-IP>} all count.
|
||||
local ip_re='\$\{?VM_IP(:-[^}]*)?\}?'
|
||||
pattern=$(grep -oE "https?://${ip_re}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?" "$tree/$SCRIPT" | head -n1)
|
||||
if [[ -n "$pattern" ]]; then
|
||||
[[ -n "$ct_ip" ]] || fail "no IP address within ${VM_BOOT_WAIT}s, so ${pattern} could not be checked"
|
||||
url=$(sed -E "s#${ip_re}#${ct_ip}#" <<<"$pattern")
|
||||
for ((waited = 0; waited < VM_URL_WAIT; waited += 10)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while its web UI was awaited"
|
||||
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
|
||||
sleep 10
|
||||
done
|
||||
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot screenshot.png
|
||||
else
|
||||
# Let the console settle on a login prompt rather than a boot message.
|
||||
sleep 20
|
||||
fi
|
||||
[[ -s "$OUT_DIR/screenshot.png" ]] || vm_console_shot
|
||||
if [[ -n "$url" && ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
fail "${url} answered ${http_code/#000/nothing} instead of 200 within ${VM_URL_WAIT}s"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} stopped by itself"
|
||||
|
||||
status="pass"
|
||||
finish
|
||||
}
|
||||
@@ -22,90 +22,112 @@ jobs:
|
||||
const now = new Date();
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// --- When stale label is added, comment immediately ---
|
||||
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.
|
||||
if (context.eventName === "pull_request_target" && context.payload.action === "labeled") {
|
||||
const label = context.payload.label?.name;
|
||||
if (label === "stale") {
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const pr = context.payload.pull_request;
|
||||
const labels = (pr.labels || []).map(l => l.name);
|
||||
if (context.payload.label?.name === "stale" && !labels.includes(TEMPLATE_LABEL)) {
|
||||
await github.rest.issues.createComment({
|
||||
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.`
|
||||
owner, repo, issue_number: pr.number, body: staleNotice(pr.user.login)
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Scheduled run: check all stale PRs ---
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
|
||||
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
|
||||
|
||||
for (const pr of prs) {
|
||||
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
|
||||
});
|
||||
|
||||
// Find the most recent time the stale label was added
|
||||
const staleLabelEvents = events
|
||||
.filter(e => e.event === "labeled" && e.label?.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 labels = pr.labels.map(l => l.name);
|
||||
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"
|
||||
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 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
|
||||
.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" });
|
||||
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;
|
||||
}
|
||||
// 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).`
|
||||
});
|
||||
}
|
||||
|
||||
// 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" });
|
||||
}
|
||||
@@ -5,8 +5,8 @@ name: Sync ct/install to Incus
|
||||
#
|
||||
# The actual mirroring + transformation lives in community-scripts/Incus
|
||||
# (.github/workflows/sync-scripts.yml): it rewrites each script's bootstrap line
|
||||
# to load the engine from community-scripts/core (ProxmoxVE's misc/build.func is
|
||||
# a monolith with no Incus backend), so a plain 1:1 copy would break every Incus
|
||||
# to load the engine from community-scripts/core (ct scripts here load its PVE
|
||||
# backend; Incus needs the Incus one), so a plain 1:1 copy would break every Incus
|
||||
# script. We therefore only fire a repository_dispatch and let that single
|
||||
# source of truth do the work (and auto-approve + merge its own PR).
|
||||
#
|
||||
|
||||
@@ -9,13 +9,13 @@ For detailed coding standards and full documentation, visit **[community-scripts
|
||||
## How Can I Help?
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **New scripts** must always be submitted to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) first — not to this repository.
|
||||
> **New scripts** must always be submitted to [DevScripts](https://github.com/community-scripts/DevScripts) first — not to this repository.
|
||||
> PRs with new scripts opened directly against ProxmoxVE **will be closed without review**.
|
||||
> **Bug fixes, improvements, and features for existing scripts** go here (ProxmoxVE).
|
||||
|
||||
| I want to… | Where to go |
|
||||
| :------------------------------------------ | :------------------------------------------------------------------------------------------- |
|
||||
| **Add a brand-new script** | [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) — testing repo for new scripts |
|
||||
| **Add a brand-new script** | [DevScripts](https://github.com/community-scripts/DevScripts) — testing repo for new scripts |
|
||||
| **Fix a bug or improve an existing script** | This repo (ProxmoxVE) — open a PR here |
|
||||
| **Add a feature to an existing script** | This repo (ProxmoxVE) — open a PR here |
|
||||
| Report a bug or broken script | [Open an Issue](https://github.com/community-scripts/ProxmoxVE/issues) |
|
||||
@@ -55,13 +55,13 @@ Use existing scripts in [`ct/`](ct/) and [`install/`](install/) as reference. Fu
|
||||
|
||||
New scripts are **not accepted directly in this repository**. The workflow is:
|
||||
|
||||
1. Fork [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) and clone it
|
||||
1. Fork [DevScripts](https://github.com/community-scripts/DevScripts) and clone it
|
||||
2. Create a branch: `git switch -c feat/myapp`
|
||||
3. Write your two script files:
|
||||
- `ct/myapp.sh`
|
||||
- `install/myapp-install.sh`
|
||||
4. Test thoroughly in ProxmoxVED — run the script against a real Proxmox instance
|
||||
5. Open a PR in **ProxmoxVED** for review and testing
|
||||
4. Test thoroughly in DevScripts — run the script against a real Proxmox instance
|
||||
5. Open a PR in **DevScripts** for review and testing
|
||||
6. Once accepted and verified there, the script will be promoted to ProxmoxVE by maintainers
|
||||
|
||||
Follow the coding standards at [community-scripts.org/docs/contribution](https://community-scripts.org/docs/contribution).
|
||||
@@ -191,7 +191,6 @@ create` still ran and the container was still built.
|
||||
## Notes
|
||||
|
||||
- **Website metadata** (name, description, logo, tags) is managed via the website — use the "Report Issue" link on any script page to request changes. Do not submit metadata changes via repo files.
|
||||
- **JSON files** in `json/` define script properties used by the website. See existing files for structure reference.
|
||||
- Keep PRs small and focused. One fix or feature per PR is ideal.
|
||||
- PRs with **new scripts** opened against ProxmoxVE will be closed — submit them to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) instead.
|
||||
- PRs with **new scripts** opened against ProxmoxVE will be closed — submit them to [DevScripts](https://github.com/community-scripts/DevScripts) instead.
|
||||
- PRs that fail CI checks will not be merged.
|
||||
@@ -1,16 +1,17 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png" height="112px" alt="Proxmox VE Helper-Scripts Logo" />
|
||||
<a href=".github/assets/community-scripts.svg"><img src=".github/assets/community-scripts.png" width="112" height="112" alt="Community Scripts — Proxmox VE logo" /></a>
|
||||
<!-- Editable artwork: .github/assets/community-scripts.svg; PNG export: 512 × 512. -->
|
||||
|
||||
<h1>Proxmox VE Helper-Scripts</h1>
|
||||
<p><strong>One-command installations for services, containers, and VMs on Proxmox VE</strong><br/>
|
||||
<h1>Community Scripts — Proxmox VE</h1>
|
||||
<p><strong>One-command installations for services, containers and VMs on Proxmox VE</strong><br/>
|
||||
A community project — built on the foundation of <a href="https://github.com/tteck">@tteck</a>'s original work</p>
|
||||
|
||||
<p>
|
||||
<a href="https://community-scripts.org"><img src="https://img.shields.io/badge/Website-community--scripts.org-4c9b3f?style=flat-square" /></a>
|
||||
<a href="https://discord.gg/3AnUqsXnmK"><img src="https://img.shields.io/badge/Discord-Join_us-7289da?style=flat-square&logo=discord&logoColor=white" /></a>
|
||||
<a href="https://github.com/community-scripts/ProxmoxVE/stargazers"><img src="https://img.shields.io/github/stars/community-scripts/ProxmoxVE?style=flat-square&label=Stars&color=f5a623" /></a>
|
||||
<a href="https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md"><img src="https://img.shields.io/badge/Changelog-view-6c5ce7?style=flat-square" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" /></a>
|
||||
<a href="https://community-scripts.org"><img src="https://img.shields.io/badge/Website-community--scripts.org-4c9b3f?style=flat-square" alt="Website" /></a>
|
||||
<a href="https://discord.gg/3AnUqsXnmK"><img src="https://img.shields.io/badge/Discord-Join_us-7289da?style=flat-square&logo=discord&logoColor=white" alt="Discord" /></a>
|
||||
<a href="https://github.com/community-scripts/ProxmoxVE/stargazers"><img src="https://img.shields.io/github/stars/community-scripts/ProxmoxVE?style=flat-square&label=Stars&color=f5a623" alt="Stars" /></a>
|
||||
<a href="https://github.com/community-scripts/ProxmoxVE/blob/main/CHANGELOG.md"><img src="https://img.shields.io/badge/Changelog-view-6c5ce7?style=flat-square" alt="Changelog" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=flat-square" alt="License" /></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -18,94 +19,146 @@
|
||||
|
||||
## What is this?
|
||||
|
||||
**Simplify your Proxmox VE setup with community-driven automation scripts.**
|
||||
**The canonical application scripts.** More than 700 of them, covering home
|
||||
automation, media, networking, databases, monitoring, security and developer
|
||||
tooling. Paste one command into your Proxmox shell and the container or VM is
|
||||
built, configured and running.
|
||||
|
||||
Install and configure popular self-hosted services with a single command — no manual package hunting, no config file archaeology. Paste a command into your Proxmox shell, answer a few prompts, and your container or VM is up and running.
|
||||
A script only describes *what* to install. Creating the container, asking the
|
||||
questions and handling the errors is the job of the shared engine in
|
||||
[core](https://github.com/community-scripts/core), which every script loads at
|
||||
start-up. The same scripts run unchanged on an Incus host through
|
||||
[the Incus repository](https://github.com/community-scripts/Incus).
|
||||
|
||||
The collection covers hundreds of services across categories like home automation, media servers, networking tools, databases, monitoring stacks, and more.
|
||||
Ready-made commands for every application are on
|
||||
[community-scripts.org](https://community-scripts.org).
|
||||
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Go to **[community-scripts.org](https://community-scripts.org)**
|
||||
2. Search for the service you want — Home Assistant, Nginx Proxy Manager, Jellyfin
|
||||
3. Copy the one-line install command from the script page
|
||||
4. Open your **Proxmox shell** and paste it
|
||||
5. Choose **Default** or **Advanced** and follow the prompts
|
||||
|
||||
Each script page lists what the container includes, the default resources and
|
||||
the post-install notes.
|
||||
|
||||
> These commands run as root on your host. Every script is readable in this
|
||||
> repository, and each script page links to its source — read it before you
|
||||
> paste it, as you would with any installer.
|
||||
|
||||
---
|
||||
|
||||
## How scripts work
|
||||
|
||||
**Default** picks sensible resources and asks only what it has to. Most installs
|
||||
finish in a few minutes.
|
||||
|
||||
**Advanced** hands you container settings, networking, storage backend and
|
||||
application options before anything is created.
|
||||
|
||||
Every container gets an `update` command. Type it in the container console:
|
||||
|
||||
```bash
|
||||
update
|
||||
```
|
||||
|
||||
It updates the installed application and reaches settings that would otherwise
|
||||
mean editing config files by hand. VM scripts have no built-in updater — use the
|
||||
guest's own package manager.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| Component | Details |
|
||||
| -------------- | ------------------------------------------------ |
|
||||
| **Proxmox VE** | Version 8.4, 9.0, 9.1, or 9.2 |
|
||||
| **Host OS** | Proxmox VE (Debian-based) |
|
||||
| **Access** | Root shell access on the Proxmox host |
|
||||
| **Network** | Internet connection required during installation |
|
||||
| Component | Details |
|
||||
| --------- | ------- |
|
||||
| **Proxmox VE** | 8.0 – 8.4, or 9.0 – 9.2 |
|
||||
| **Access** | Root shell on the Proxmox host |
|
||||
| **Network** | Internet access during installation |
|
||||
|
||||
Virtual machine scripts need the same versions as containers.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
## Repository map
|
||||
|
||||
The fastest way to find and run scripts:
|
||||
|
||||
1. Go to **[community-scripts.org](https://community-scripts.org)**
|
||||
2. Search for the service you want (e.g. "Home Assistant", "Nginx Proxy Manager", "Jellyfin")
|
||||
3. Copy the one-line install command from the script page
|
||||
4. Open your **Proxmox Shell** and paste it
|
||||
5. Choose between **Default** or **Advanced** setup and follow the prompts
|
||||
|
||||
Each script page documents what the container includes, default resource allocation, and post-install notes.
|
||||
| Repository | Contains |
|
||||
| ---------- | -------- |
|
||||
| [core](https://github.com/community-scripts/core) | The engine — container and VM creation, the wizard, error handling |
|
||||
| **ProxmoxVE** (this repo) | Canonical application scripts, plus `tools/pve/`, `vm/` and `turnkey/` |
|
||||
| [Incus](https://github.com/community-scripts/Incus) | The same scripts on an Incus host, plus Incus VMs and host tooling |
|
||||
| [DevScripts](https://github.com/community-scripts/DevScripts) | Where new scripts are tested before they land here |
|
||||
|
||||
---
|
||||
|
||||
## How Scripts Work
|
||||
## Layout
|
||||
|
||||
Every script follows the same pattern:
|
||||
| Folder | Contents |
|
||||
| ------ | -------- |
|
||||
| `ct/` | Application scripts — the command you paste |
|
||||
| `install/` | What each script installs inside the container |
|
||||
| `vm/` | Virtual machine scripts |
|
||||
| `tools/pve/` | Host tooling: post-install, hardware passthrough, maintenance |
|
||||
| `turnkey/` | TurnKey Linux appliance helper |
|
||||
|
||||
**Default mode** — Picks sensible resource defaults (CPU, RAM, storage) and asks only the minimum required questions. Most installs finish in under five minutes.
|
||||
|
||||
**Advanced mode** — Gives you full control over container settings, networking, storage backends, and application-level configuration before anything is installed.
|
||||
|
||||
After installation, each container ships with a **post-install helper** accessible from the Proxmox shell. It handles common tasks like:
|
||||
|
||||
- Applying updates to the installed service
|
||||
- Changing application settings without manually editing config files
|
||||
- Basic troubleshooting and log access
|
||||
Application metadata — name, description, logo, tags — is managed on the website,
|
||||
not in this repository. Use the **Report Issue** link on any script page to
|
||||
request a change.
|
||||
|
||||
---
|
||||
|
||||
## What's Included
|
||||
## Testing a fork or branch
|
||||
|
||||
The repository covers a wide range of categories. A few examples:
|
||||
Engine and scripts resolve independently, so either can point somewhere else.
|
||||
Put the variable on the command itself — nothing needs exporting.
|
||||
|
||||
| Category | Examples |
|
||||
| --------------- | --------------------------------------------------- |
|
||||
| Home Automation | Home Assistant, Zigbee2MQTT, ESPHome, Node-RED |
|
||||
| Media | Jellyfin, Plex, Radarr, Sonarr, Immich |
|
||||
| Networking | AdGuard Home, Nginx Proxy Manager, Pi-hole, Traefik |
|
||||
| Monitoring | Grafana, Prometheus, Uptime Kuma, Netdata |
|
||||
| Databases | PostgreSQL, MariaDB, Redis, InfluxDB |
|
||||
| Security | Vaultwarden, CrowdSec, Authentik |
|
||||
| Dev & Tools | Gitea, Portainer, VS Code Server, n8n |
|
||||
To run your own copy of the scripts:
|
||||
|
||||
> Browse the full list at **[community-scripts.org/categories](https://community-scripts.org/categories)** — new scripts are added regularly.
|
||||
```bash
|
||||
COMMUNITY_SCRIPTS_URL=https://raw.githubusercontent.com/YOU/ProxmoxVE/my-branch \
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/YOU/ProxmoxVE/my-branch/ct/debian.sh)"
|
||||
```
|
||||
|
||||
A `ct/` script fetched with `curl` cannot tell the engine where it came from, so
|
||||
without that variable it would look for its `install/` half upstream.
|
||||
|
||||
Only when you are changing the engine itself:
|
||||
|
||||
```bash
|
||||
COMMUNITY_SCRIPTS_CORE_URL=https://raw.githubusercontent.com/YOU/core/my-branch \
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"
|
||||
```
|
||||
|
||||
A local checkout of core wins over both — point at it with
|
||||
`COMMUNITY_SCRIPTS_CORE_DIR`.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
This project runs on community contributions. Whether you want to write new scripts, improve existing ones, or just report a bug — every bit helps.
|
||||
This project runs on community contributions. New scripts, fixes, bug reports —
|
||||
all of it helps.
|
||||
|
||||
### Where to start
|
||||
|
||||
| I want to… | Go here |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| Add a **new** script | [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) — new scripts are tested here first |
|
||||
| Fix or improve an **existing** script | [Contributing Guidelines](CONTRIBUTING.md) — open a PR in this repo |
|
||||
| Report a bug or broken script | [Issues](https://github.com/community-scripts/ProxmoxVE/issues) |
|
||||
| Request a new script or feature | [Discussions](https://github.com/community-scripts/ProxmoxVE/discussions) |
|
||||
| Report a security vulnerability | [Security Policy](SECURITY.md) |
|
||||
| Get help or chat with other users | [Discord](https://discord.gg/3AnUqsXnmK) |
|
||||
| I want to… | Go here |
|
||||
| ---------- | ------- |
|
||||
| Add a **new** script | [DevScripts](https://github.com/community-scripts/DevScripts) — new scripts are tested there first |
|
||||
| Fix or improve an **existing** script | [Contributing Guidelines](CONTRIBUTING.md) — open a PR here |
|
||||
| Change how containers are built | [core](https://github.com/community-scripts/core) |
|
||||
| Report a bug or a broken script | [Issues](https://github.com/community-scripts/ProxmoxVE/issues) |
|
||||
| Request a script or a feature | [Discussions](https://github.com/community-scripts/ProxmoxVE/discussions) |
|
||||
| Report a security vulnerability | [Security Policy](SECURITY.md) |
|
||||
| Ask a question | [Discord](https://discord.gg/3AnUqsXnmK) |
|
||||
|
||||
### Before you open a PR
|
||||
|
||||
- **New scripts go to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED), not here.** PRs with new scripts opened directly against this repo will be closed.
|
||||
- Bug fixes and improvements to existing scripts belong in this repo — read the [Contributing Guidelines](CONTRIBUTING.md) first.
|
||||
- Keep PRs focused. One fix or feature per PR.
|
||||
- Document what your script installs and any non-obvious decisions in the corresponding JSON metadata file.
|
||||
- **New scripts go to [DevScripts](https://github.com/community-scripts/DevScripts), not here.** PRs adding new scripts to this repository will be closed.
|
||||
- Keep a PR to one fix or one feature.
|
||||
- A bug that also happens on Incus is an application or engine bug — say so in the report.
|
||||
- Read the [Contributing Guidelines](CONTRIBUTING.md) before your first PR.
|
||||
|
||||
---
|
||||
|
||||
@@ -137,6 +190,8 @@ This project runs on community contributions. Whether you want to write new scri
|
||||
<sub><b>CrazyWolf13</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/tremor021">
|
||||
<img src="https://github.com/tremor021.png" width="80" height="80" style="border-radius:50%" alt="tremor021" /><br/>
|
||||
@@ -155,6 +210,7 @@ This project runs on community contributions. Whether you want to write new scri
|
||||
<sub><b>asylumexp</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -194,9 +250,11 @@ This project runs on community contributions. Whether you want to write new scri
|
||||
|
||||
## Support the Project
|
||||
|
||||
This project is maintained by volunteers. All infrastructure costs come out of pocket, and the work is done in people's spare time.
|
||||
This project is maintained by volunteers. Infrastructure costs come out of
|
||||
pocket and the work happens in people's spare time.
|
||||
|
||||
**30% of all donations are forwarded directly to cancer research and hospice care** — a cause that was important to tteck.
|
||||
**30% of all donations are forwarded directly to cancer research and hospice
|
||||
care** — a cause that was important to tteck.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://ko-fi.com/community_scripts">
|
||||
@@ -212,14 +270,13 @@ This project is maintained by volunteers. All infrastructure costs come out of p
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE) — free to use, modify, and redistribute for personal and commercial purposes.
|
||||
|
||||
See the full license text in [LICENSE](LICENSE).
|
||||
MIT — free to use, modify and redistribute, for personal and commercial
|
||||
purposes. See [LICENSE](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>Built on the foundation of <a href="https://github.com/tteck">tteck</a>'s original work · <a href="https://github.com/tteck/Proxmox">Original Repository</a></sub><br/>
|
||||
<sub>Maintained and expanded by the community · In memory of tteck</sub><br/>
|
||||
<sub><i>Proxmox® is a registered trademark of <a href="https://www.proxmox.com/en/about/company">Proxmox Server Solutions GmbH</a></i></sub>
|
||||
<sub><i>This is an independent community project. <b>Proxmox</b>® is a registered trademark of <a href="https://www.proxmox.com/en/about/company">Proxmox Server Solutions GmbH</a>.</i></sub>
|
||||
</div>
|
||||
@@ -37,9 +37,9 @@ function update_script() {
|
||||
msg_ok "Services Stopped"
|
||||
|
||||
create_backup /opt/adventurelog/backend/server/.env \
|
||||
/opt/adventurelog/backend/server/media
|
||||
/opt/adventurelog/frontend/.env
|
||||
|
||||
fetch_and_deploy_gh_release "adventurelog" "seanmorley15/adventurelog" "tarball"
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="backend/server/media" fetch_and_deploy_gh_release "adventurelog" "seanmorley15/adventurelog" "tarball"
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
msg_info "Ensuring PostgreSQL Extensions"
|
||||
@@ -56,7 +56,6 @@ function update_script() {
|
||||
fi
|
||||
$STD .venv/bin/python -m pip install --upgrade pip
|
||||
$STD .venv/bin/python -m pip install -r requirements.txt
|
||||
$STD .venv/bin/python -m pip install 'djangorestframework<3.15'
|
||||
$STD .venv/bin/python -m manage collectstatic --noinput
|
||||
$STD .venv/bin/python -m manage migrate
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/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://aws.amazon.com/linux/
|
||||
|
||||
APP="Amazon Linux"
|
||||
var_tags="${var_tags:-os}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-amazonlinux}"
|
||||
var_version="${var_version:-2023}"
|
||||
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
|
||||
msg_info "Updating Amazon Linux LXC"
|
||||
$STD dnf -y upgrade
|
||||
msg_ok "Updated Amazon Linux LXC"
|
||||
cleanup_lxc
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!"
|
||||
msg_custom "🚀" "${GN}" "${APP} setup has been successfully initialized!"
|
||||
@@ -39,6 +39,7 @@ function update_script() {
|
||||
create_backup /opt/anchor/.env
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anchor" "ZhFahim/anchor" "tarball"
|
||||
NODE_VERSION="26" NODE_MODULE="pnpm" setup_nodejs
|
||||
|
||||
msg_info "Building Server"
|
||||
cd /opt/anchor/server
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Engine comes from community-scripts/core; this repo only ships the scripts.
|
||||
# A local core checkout wins (COMMUNITY_SCRIPTS_CORE_DIR, else a sibling ../core),
|
||||
# so a fork or branch of core can be tested without editing this file.
|
||||
_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/Mintplex-Labs/anything-llm
|
||||
|
||||
APP="AnythingLLM"
|
||||
var_tags="${var_tags:-ai;rag}"
|
||||
var_cpu="${var_cpu:-4}"
|
||||
var_ram="${var_ram:-6144}"
|
||||
var_disk="${var_disk:-20}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_gpu="${var_gpu:-yes}"
|
||||
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
|
||||
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/anythingllm ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "anythingllm" "Mintplex-Labs/anything-llm"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop anythingllm anythingllm-collector
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
create_backup /opt/anythingllm/server/.env /opt/anythingllm/collector/.env /opt/anythingllm/frontend/.env
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anythingllm" "Mintplex-Labs/anything-llm" "tarball"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Building AnythingLLM (Patience)"
|
||||
cd /opt/anythingllm
|
||||
export PUPPETEER_SKIP_DOWNLOAD=true
|
||||
export NODE_OPTIONS="--max-old-space-size=3072"
|
||||
$STD yarn setup
|
||||
cd /opt/anythingllm/frontend
|
||||
$STD yarn build
|
||||
rm -rf /opt/anythingllm/server/public
|
||||
cp -R /opt/anythingllm/frontend/dist /opt/anythingllm/server/public
|
||||
cd /opt/anythingllm/server
|
||||
$STD npx prisma generate --schema=./prisma/schema.prisma
|
||||
$STD npx prisma migrate deploy --schema=./prisma/schema.prisma
|
||||
msg_ok "Built AnythingLLM"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start anythingllm anythingllm-collector
|
||||
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}:3001${CL}"
|
||||
@@ -37,13 +37,25 @@ function update_script() {
|
||||
systemctl stop anytype
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/anytype/data
|
||||
create_backup /opt/anytype/data /opt/anytype/.env
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_$(arch_resolve).tar.gz"
|
||||
chmod +x /opt/anytype/any-sync-bundle
|
||||
|
||||
restore_backup
|
||||
|
||||
if [[ ! -f /opt/anytype/.env ]]; then
|
||||
cat <<EOF >/opt/anytype/.env
|
||||
ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml
|
||||
ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml
|
||||
ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/
|
||||
ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=$(hostname -I | awk '{print $1}')
|
||||
ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/
|
||||
ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/
|
||||
ANY_SYNC_BUNDLE_LOG_LEVEL=info
|
||||
EOF
|
||||
fi
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start anytype
|
||||
msg_ok "Started Service"
|
||||
|
||||
@@ -51,40 +51,37 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Stopping Tomcat service"
|
||||
systemctl stop tomcat
|
||||
msg_ok "Stopped Tomcat service"
|
||||
|
||||
create_backup $TOMCAT_DIR/conf
|
||||
[[ -d $TOMCAT_DIR/webapps ]] && create_backup $TOMCAT_DIR/webapps
|
||||
[[ -d $TOMCAT_DIR/lib ]] && create_backup $TOMCAT_DIR/lib
|
||||
|
||||
msg_info "Downloading Tomcat ${LATEST_VERSION}"
|
||||
TOMCAT_URL="https://dlcdn.apache.org/tomcat/tomcat-${TOMCAT_MAJOR}/v${LATEST_VERSION}/bin/apache-tomcat-${LATEST_VERSION}.tar.gz"
|
||||
curl -fsSL "$TOMCAT_URL" -o /tmp/tomcat-update.tar.gz
|
||||
msg_ok "Downloaded Tomcat ${LATEST_VERSION}"
|
||||
|
||||
# Keep only the jars in lib that the new release does not ship (JDBC drivers
|
||||
# and the like). ecj and jakartaee-migration are Tomcat's own and change name
|
||||
# with each version, so they are never carried over.
|
||||
TOMCAT_FILES=$(tar -tzf /tmp/tomcat-update.tar.gz)
|
||||
BACKUP_PATHS=("$TOMCAT_DIR/conf")
|
||||
[[ -d $TOMCAT_DIR/webapps ]] && BACKUP_PATHS+=("$TOMCAT_DIR/webapps")
|
||||
for jar in "$TOMCAT_DIR"/lib/*.jar; do
|
||||
[[ -f $jar ]] || continue
|
||||
case "${jar##*/}" in ecj-*.jar | jakartaee-migration-*.jar) continue ;; esac
|
||||
grep -qxF "apache-tomcat-${LATEST_VERSION}/lib/${jar##*/}" <<<"$TOMCAT_FILES" || BACKUP_PATHS+=("$jar")
|
||||
done
|
||||
|
||||
msg_info "Stopping Tomcat service"
|
||||
systemctl stop tomcat
|
||||
msg_ok "Stopped Tomcat service"
|
||||
|
||||
create_backup "${BACKUP_PATHS[@]}"
|
||||
|
||||
msg_info "Installing update"
|
||||
rm -rf "${TOMCAT_DIR:?}"/*
|
||||
tar --strip-components=1 -xzf /tmp/tomcat-update.tar.gz -C "$TOMCAT_DIR"
|
||||
rm -f /tmp/tomcat-update.tar.gz
|
||||
msg_ok "Installed update"
|
||||
|
||||
msg_info "Restoring configuration and applications"
|
||||
cp -a "$BACKUP_DIR/conf"/* "$TOMCAT_DIR/conf/"
|
||||
cp -a "$BACKUP_DIR/webapps"/* "$TOMCAT_DIR/webapps/" 2>/dev/null || true
|
||||
if [[ -d "$BACKUP_DIR/lib" ]]; then
|
||||
for jar in "$BACKUP_DIR/lib"/*.jar; do
|
||||
[[ -f $jar ]] || continue
|
||||
jar_name=$(basename "$jar")
|
||||
if [[ ! -f "$TOMCAT_DIR/lib/$jar_name" ]]; then
|
||||
cp "$jar" "$TOMCAT_DIR/lib/"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
rm -rf "$BACKUP_DIR"
|
||||
restore_backup
|
||||
chown -R root:root "$TOMCAT_DIR"
|
||||
msg_ok "Restored configuration and applications"
|
||||
|
||||
msg_info "Starting Tomcat service"
|
||||
systemctl start tomcat
|
||||
|
||||
@@ -46,7 +46,7 @@ function update_script() {
|
||||
$STD uv venv /opt/apprise/.venv
|
||||
$STD uv pip install -r requirements.txt gunicorn supervisor -p /opt/apprise/.venv/bin/python
|
||||
ln -sf /opt/apprise/.venv/bin/supervisord /opt/apprise/.venv/bin/gunicorn /usr/local/bin/
|
||||
cp -fr apprise_api/static /usr/share/nginx/html/s/
|
||||
cp -fr apprise_api/static/. /usr/share/nginx/html/s/
|
||||
mv apprise_api/ webapp
|
||||
touch /etc/nginx/server-override.conf
|
||||
touch /etc/nginx/location-override.conf
|
||||
|
||||
@@ -58,7 +58,7 @@ function update_script() {
|
||||
|
||||
msg_info "Updating Python Environment (${AUDIOMUSE_BACKEND})"
|
||||
cd /opt/audiomuse-ai
|
||||
$STD uv venv --seed --python "$PY_VERSION" /opt/audiomuse-ai/.venv
|
||||
$STD uv venv --clear --seed --python "$PY_VERSION" /opt/audiomuse-ai/.venv
|
||||
$STD uv pip install --python /opt/audiomuse-ai/.venv \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_COMMON}" \
|
||||
-r "/opt/audiomuse-ai/requirements/${REQ_ACCEL}"
|
||||
|
||||
@@ -37,19 +37,16 @@ function update_script() {
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "aurral" "lklynet/aurral" "tarball"
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
NODE_VERSION="26" NODE_MODULE="pnpm" setup_nodejs
|
||||
ensure_dependencies git
|
||||
|
||||
msg_info "Updating Aurral"
|
||||
cd /opt/aurral
|
||||
export VITE_APP_VERSION="$(cat ~/.aurral)"
|
||||
export VITE_GITHUB_REPO="lklynet/aurral"
|
||||
export VITE_RELEASE_CHANNEL="stable"
|
||||
# aurral's .npmrc sets engine-strict=true and pins an exact node patch (e.g. 26.8.x),
|
||||
# which NodeSource can't match since it only ships the latest patch per major.
|
||||
export npm_config_engine_strict=false
|
||||
$STD npm ci --workspace frontend --include-workspace-root=false
|
||||
$STD npm run build --workspace frontend
|
||||
$STD npm ci --workspace backend --omit=dev --include=optional --include-workspace-root=false
|
||||
$STD pnpm install --frozen-lockfile --config.engine-strict=false --filter aurral-frontend --filter aurral-backend
|
||||
$STD pnpm --filter aurral-frontend run build
|
||||
mkdir -p /opt/aurral_data
|
||||
cat <<EOF >/opt/aurral/aurral.env
|
||||
NODE_ENV=production
|
||||
@@ -57,6 +54,8 @@ PORT=3001
|
||||
AURRAL_DATA_DIR=/opt/aurral_data
|
||||
APP_VERSION=$(cat ~/.aurral)
|
||||
EOF
|
||||
sed -i 's#backend/server.js#backend/server.ts#' /etc/systemd/system/aurral.service
|
||||
systemctl daemon-reload
|
||||
msg_ok "Updated Aurral"
|
||||
|
||||
msg_info "Starting Service"
|
||||
|
||||
@@ -47,7 +47,7 @@ function update_script() {
|
||||
RUST_PROFILE="minimal" RUST_TOOLCHAIN="stable" setup_rust
|
||||
setup_yq
|
||||
|
||||
AUTHENTIK_VERSION="version/2026.8.3"
|
||||
AUTHENTIK_VERSION="version/2026.8.5"
|
||||
# Source: https://github.com/goauthentik/fips/blob/main/Makefile#L26
|
||||
XMLSEC_VERSION="1.3.12"
|
||||
|
||||
@@ -155,7 +155,8 @@ function update_script() {
|
||||
rm -r /opt/authentik/blueprints
|
||||
$STD find /opt/authentik-data -path '*/lost+found' -prune -o -exec chown authentik:authentik {} +
|
||||
|
||||
if [[ $MAJOR == 2026 && $MINOR -lt 8 ]]; then
|
||||
# Also catch installs whose version marker already reads 2026.8.x but whose units were never migrated
|
||||
if [[ $MAJOR == 2026 && $MINOR -lt 8 ]] || grep -q "ExecStart=/opt/authentik/authentik-server" /etc/systemd/system/authentik-server.service; then
|
||||
msg_info "Updating Worker and Server config (from $MAJOR.$MINOR)"
|
||||
cat <<EOF >>/etc/default/authentik-server
|
||||
RUST_BACKTRACE=full
|
||||
|
||||
@@ -46,11 +46,19 @@ function update_script() {
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Refreshing Bar-Assistant data pack"
|
||||
rm -rf /opt/bar-assistant/resources/data
|
||||
mkdir -p /opt/bar-assistant/resources/data
|
||||
curl -fsSL https://github.com/bar-assistant/data/archive/refs/heads/v5.tar.gz | tar -xz --strip-components=1 -C /opt/bar-assistant/resources/data
|
||||
chown -R www-data:www-data /opt/bar-assistant/resources/data
|
||||
msg_ok "Refreshed Bar-Assistant data pack"
|
||||
|
||||
msg_info "Configuring Bar-Assistant"
|
||||
cd /opt/bar-assistant
|
||||
$STD composer install --no-interaction
|
||||
$STD php artisan migrate --force
|
||||
$STD php artisan storage:link
|
||||
$STD php artisan starter-media:publish
|
||||
$STD php artisan bar:setup-meilisearch
|
||||
$STD php artisan scout:sync-index-settings
|
||||
$STD php artisan config:cache
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/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/blinkospace/blinko
|
||||
|
||||
APP="Blinko"
|
||||
var_tags="${var_tags:-notes;ai;knowledge}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-6144}"
|
||||
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/blinko ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "blinko" "blinkospace/blinko"; then
|
||||
msg_info "Stopping Blinko"
|
||||
systemctl stop blinko
|
||||
msg_ok "Stopped Blinko"
|
||||
|
||||
NODE_VERSION="24" NODE_MODULE="bun" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "blinko" "blinkospace/blinko" "tarball"
|
||||
|
||||
msg_info "Building Blinko"
|
||||
cd /opt/blinko
|
||||
$STD bun install
|
||||
$STD bunx prisma generate
|
||||
$STD bun run build:web
|
||||
$STD bun run build:seed
|
||||
mv dist/public server/public
|
||||
cp -r server/lute.min.js server/vditor dist/
|
||||
ln -s /opt/blinko_data/storage /opt/blinko/.blinko
|
||||
msg_ok "Built Blinko"
|
||||
|
||||
msg_info "Starting Blinko"
|
||||
systemctl start blinko
|
||||
msg_ok "Started Blinko"
|
||||
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}:1111${CL}"
|
||||
@@ -48,7 +48,12 @@ function update_script() {
|
||||
|
||||
$STD corepack prepare "pnpm@${PNPM_VERSION}" --activate
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm --filter client run build-only
|
||||
# vite's bundler (rolldown) crashes at random on some builds, rolldown#10860.
|
||||
for attempt in 1 2 3; do
|
||||
$STD pnpm --filter client run build-only && break
|
||||
[[ $attempt -eq 3 ]] && { msg_error "Client build failed three times"; exit 1; }
|
||||
msg_warn "Client build failed (attempt $attempt), retrying"
|
||||
done
|
||||
$STD pnpm --filter server run build
|
||||
cp -r /opt/bookorbit/client/dist /opt/bookorbit/server/public
|
||||
mkdir -p /opt/bookorbit/server/migrations
|
||||
|
||||
@@ -69,7 +69,7 @@ function update_script() {
|
||||
|
||||
if [[ "$(cat /opt/borg1-venv/.pinned_version 2>/dev/null)" != "$BORG1_VERSION" ]]; then
|
||||
msg_info "Installing Borg ${BORG1_VERSION} (Patience)"
|
||||
$STD uv venv --python "$BORG_PYTHON" /opt/borg1-venv
|
||||
$STD uv venv --clear --python "$BORG_PYTHON" /opt/borg1-venv
|
||||
$STD uv pip install --python /opt/borg1-venv pyfuse3 "borgbackup==${BORG1_VERSION}"
|
||||
echo "$BORG1_VERSION" >/opt/borg1-venv/.pinned_version
|
||||
ln -sf /opt/borg1-venv/bin/borg /usr/local/bin/borg
|
||||
@@ -78,7 +78,7 @@ function update_script() {
|
||||
|
||||
if [[ "$(cat /opt/borg2-venv/.pinned_version 2>/dev/null)" != "${BORG2_VERSION}-${BORGSTORE_VERSION}" ]]; then
|
||||
msg_info "Installing Borg ${BORG2_VERSION} (Patience)"
|
||||
$STD uv venv --python "$BORG_PYTHON" /opt/borg2-venv
|
||||
$STD uv venv --clear --python "$BORG_PYTHON" /opt/borg2-venv
|
||||
$STD uv pip install --python /opt/borg2-venv pyfuse3 "borgbackup==${BORG2_VERSION}" "borgstore[rclone,sftp,rest,s3,blake3]==${BORGSTORE_VERSION}"
|
||||
echo "${BORG2_VERSION}-${BORGSTORE_VERSION}" >/opt/borg2-venv/.pinned_version
|
||||
ln -sf /opt/borg2-venv/bin/borg /usr/local/bin/borg2
|
||||
|
||||
@@ -9,8 +9,8 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
APP="ByteStash"
|
||||
var_tags="${var_tags:-code}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
@@ -35,9 +35,8 @@ function update_script() {
|
||||
systemctl stop bytestash-backend bytestash-frontend
|
||||
msg_ok "Services Stopped"
|
||||
|
||||
[[ -d /opt/bytestash/data ]] && create_backup /opt/bytestash/data
|
||||
[[ -d /opt/data ]] && create_backup /opt/data
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bytestash" "jordan-dalby/ByteStash" "tarball"
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data" fetch_and_deploy_gh_release "bytestash" "jordan-dalby/ByteStash" "tarball"
|
||||
restore_backup
|
||||
|
||||
msg_info "Configuring ByteStash"
|
||||
@@ -50,7 +49,6 @@ function update_script() {
|
||||
msg_info "Starting Services"
|
||||
systemctl start bytestash-backend bytestash-frontend
|
||||
msg_ok "Started Services"
|
||||
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
|
||||
@@ -38,19 +38,30 @@ update_deb_based() {
|
||||
exit
|
||||
fi
|
||||
|
||||
cleanup_old_repo_files "caddy"
|
||||
msg_info "Updating Caddy LXC"
|
||||
apt_update_safe
|
||||
$STD apt upgrade -y
|
||||
msg_ok "Updated Caddy LXC"
|
||||
|
||||
[[ -f ~/.caddy ]] || dpkg-query -W -f='${Version}' caddy >~/.caddy 2>/dev/null || true
|
||||
if check_for_gh_release "caddy" "caddyserver/caddy"; then
|
||||
DPKG_FORCE_CONFOLD=1 fetch_and_deploy_gh_release "caddy" "caddyserver/caddy" "binary" "latest" "/opt/caddy" "caddy_*_linux_$(arch_resolve).deb"
|
||||
systemctl restart caddy
|
||||
fi
|
||||
|
||||
if command -v xcaddy >/dev/null 2>&1; then
|
||||
if check_for_gh_release "xcaddy" "caddyserver/xcaddy"; then
|
||||
setup_go
|
||||
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "binary"
|
||||
|
||||
msg_info "Updating xCaddy"
|
||||
$STD xcaddy build
|
||||
msg_ok "Updated xCaddy"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
msg_ok "Updated successfully!"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/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: Jelcoo
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://calagopus.com/
|
||||
|
||||
APP="Calagopus"
|
||||
var_tags="${var_tags:-panel;game-server}"
|
||||
var_cpu="${var_cpu:-4}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-40}"
|
||||
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 [[ ! -f /usr/local/bin/calagopus-panel ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "calagopus-panel" "calagopus/panel"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop calagopus-panel
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /etc/calagopus/.env /var/lib/calagopus
|
||||
|
||||
fetch_and_deploy_gh_release "calagopus-panel" "calagopus/panel" "singlefile" "latest" "/usr/local/bin" "panel-rs-aio-$(arch_resolve x86_64 aarch64)-linux"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start calagopus-panel
|
||||
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}:8000${CL}"
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/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 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://readarr.com/
|
||||
# Source: https://github.com/casdoor/casdoor
|
||||
|
||||
APP="Readarr"
|
||||
var_tags="${var_tags:-media;comic;eBook}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
APP="Casdoor"
|
||||
var_tags="${var_tags:-auth;sso;iam}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
@@ -25,15 +25,24 @@ function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /var/lib/readarr/ ]]; then
|
||||
|
||||
if [[ ! -d /opt/casdoor ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
msg_info "Updating $APP LXC"
|
||||
apt_update_safe
|
||||
$STD apt -y upgrade
|
||||
msg_ok "Updated $APP LXC"
|
||||
msg_ok "Updated successfully!"
|
||||
|
||||
if check_for_gh_release "casdoor" "casdoor/casdoor"; then
|
||||
msg_info "Stopping Casdoor"
|
||||
systemctl stop casdoor
|
||||
msg_ok "Stopped Casdoor"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "casdoor" "casdoor/casdoor" "prebuild" "latest" "/opt/casdoor" "casdoor_Linux_$(arch_resolve "x86_64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Starting Casdoor"
|
||||
systemctl start casdoor
|
||||
msg_ok "Started Casdoor"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -41,7 +50,7 @@ start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8787${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/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: fabriziosalmi
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/fabriziosalmi/certmate
|
||||
|
||||
APP="CertMate"
|
||||
var_tags="${var_tags:-ssl;certificates;acme}"
|
||||
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:-no}" # unset = ask the user; set yes/no only when verified
|
||||
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/certmate ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "certmate" "fabriziosalmi/certmate"; then
|
||||
msg_info "Stopping CertMate"
|
||||
systemctl stop certmate
|
||||
msg_ok "Stopped CertMate"
|
||||
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "certmate" "fabriziosalmi/certmate" "tarball"
|
||||
|
||||
msg_info "Installing CertMate Dependencies"
|
||||
cd /opt/certmate
|
||||
$STD uv venv --python 3.12 /opt/certmate/.venv
|
||||
$STD uv pip sync --python /opt/certmate/.venv/bin/python requirements.lock
|
||||
$STD /opt/certmate/.venv/bin/certbot --version
|
||||
msg_ok "Installed CertMate Dependencies"
|
||||
|
||||
msg_info "Starting CertMate"
|
||||
systemctl start certmate
|
||||
msg_ok "Started CertMate"
|
||||
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}:8000${CL}"
|
||||
echo -e "${INFO}${YW}The first page creates the admin account and asks for the API token: grep API_BEARER_TOKEN /opt/certmate_data/.env${CL}"
|
||||
@@ -77,7 +77,6 @@ function update_script() {
|
||||
msg_info "Updating Browserless (Patience)"
|
||||
$STD git -C /opt/browserless/ fetch --all
|
||||
$STD git -C /opt/browserless/ reset --hard origin/main
|
||||
$STD npm update --prefix /opt/browserless
|
||||
$STD npm ci --include=optional --include=dev --prefix /opt/browserless
|
||||
$STD npm install --save-exact playwright-core@1.62.1 --prefix /opt/browserless
|
||||
$STD /opt/browserless/node_modules/playwright-core/cli.js install --with-deps
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/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/chartdb/chartdb
|
||||
|
||||
APP="ChartDB"
|
||||
var_tags="${var_tags:-database;diagram;dev-tools}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-6144}"
|
||||
var_disk="${var_disk:-4}"
|
||||
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/chartdb ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "chartdb" "chartdb/chartdb"; then
|
||||
msg_info "Stopping ChartDB"
|
||||
systemctl stop nginx
|
||||
msg_ok "Stopped ChartDB"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "chartdb" "chartdb/chartdb" "tarball"
|
||||
|
||||
msg_info "Building ChartDB"
|
||||
cd /opt/chartdb
|
||||
$STD npm ci
|
||||
NODE_OPTIONS="--max-old-space-size=4096" $STD npm run build
|
||||
rm -rf /opt/chartdb/node_modules
|
||||
msg_ok "Built ChartDB"
|
||||
|
||||
msg_info "Starting ChartDB"
|
||||
systemctl start nginx
|
||||
msg_ok "Started ChartDB"
|
||||
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}https://${IP}${CL}"
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/chattocorp/chatto
|
||||
|
||||
APP="Chatto"
|
||||
var_tags="${var_tags:-chat;messaging}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/chatto ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "chatto" "chattocorp/chatto"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop chatto
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "chatto" "chattocorp/chatto" "prebuild" "latest" "/opt/chatto" "chatto_Linux_$(arch_resolve x86_64 arm64).tar.gz"
|
||||
chmod +x /opt/chatto/chatto
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start chatto
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:4000${CL}"
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/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/MoldyTaint/Cinephage
|
||||
|
||||
APP="Cinephage"
|
||||
var_tags="${var_tags:-arr;media;torrent;usenet}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-10}"
|
||||
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/cinephage ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "cinephage" "MoldyTaint/Cinephage"; then
|
||||
msg_info "Stopping Cinephage"
|
||||
systemctl stop cinephage
|
||||
msg_ok "Stopped Cinephage"
|
||||
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
|
||||
|
||||
msg_info "Building Cinephage"
|
||||
cd /opt/cinephage
|
||||
$STD npm ci
|
||||
$STD npm run build
|
||||
$STD npm prune --omit=dev
|
||||
sed -i "s/^APP_VERSION=.*/APP_VERSION=$(cat ~/.cinephage)/" /opt/cinephage_data/.env
|
||||
msg_ok "Built Cinephage"
|
||||
|
||||
msg_info "Updating Camoufox"
|
||||
$STD /opt/cinephage/node_modules/.bin/camoufox fetch
|
||||
msg_ok "Updated Camoufox"
|
||||
|
||||
msg_info "Starting Cinephage"
|
||||
systemctl start cinephage
|
||||
msg_ok "Started Cinephage"
|
||||
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}:3000${CL}"
|
||||
@@ -39,7 +39,7 @@ function update_script() {
|
||||
|
||||
create_backup /opt/cliproxyapi/config.yaml
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cliproxyapi" "router-for-me/CLIProxyAPI" "prebuild" "latest" "/opt/cliproxyapi" "CLIProxyAPI_*_linux_$(arch_resolve "amd64" "aarch64").tar.gz"
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="plugins data" fetch_and_deploy_gh_release "cliproxyapi" "router-for-me/CLIProxyAPI" "prebuild" "latest" "/opt/cliproxyapi" "CLIProxyAPI_*_linux_$(arch_resolve "amd64" "aarch64").tar.gz"
|
||||
|
||||
restore_backup
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://coolify.io/
|
||||
|
||||
APP="Coolify"
|
||||
var_tags="${var_tags:-docker;paas}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-30}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
ADDON_SCRIPT="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh"
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /data/coolify ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_warn "⚠️ ${APP} has been migrated to an addon script."
|
||||
echo ""
|
||||
echo -e "${INFO}${YW}This is a one-time migration. After this, you can update ${APP} anytime with:${CL}"
|
||||
echo -e "${TAB}${TAB}${GN}update_coolify${CL} or ${GN}bash <(curl -fsSL ${ADDON_SCRIPT})${CL}"
|
||||
echo ""
|
||||
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
|
||||
if [[ ! "${CONFIRM,,}" =~ ^(y|yes)$ ]]; then
|
||||
msg_warn "Migration skipped. The old update will continue to work for now."
|
||||
msg_info "Updating ${APP} (legacy)"
|
||||
$STD bash <(curl -fsSL https://cdn.coollabs.io/coolify/install.sh)
|
||||
msg_ok "Updated ${APP}"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_coolify 2>/dev/null || true
|
||||
msg_ok "Migration complete"
|
||||
|
||||
echo -e "${INFO}${YW}Running addon update${CL}"
|
||||
type=update bash <(curl -fsSL "${ADDON_SCRIPT}")
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
|
||||
@@ -26,7 +26,7 @@ function update_script() {
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
ensure_dependencies build-essential
|
||||
|
||||
if command -v cross-seed &>/dev/null; then
|
||||
|
||||
@@ -30,12 +30,14 @@ function update_script() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
# Updates to 2026.10.0 finished without infra.js; run them again so it gets written
|
||||
[[ -f /opt/cryptpad/config/infra.example.js && ! -f /opt/cryptpad/config/infra.js ]] && rm -f ~/.cryptpad
|
||||
if check_for_gh_release "cryptpad" "cryptpad/cryptpad"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop cryptpad
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/cryptpad/config/config.js /opt/cryptpad/www/common/onlyoffice/dist
|
||||
create_backup /opt/cryptpad/config/config.js /opt/cryptpad/config/infra.js /opt/cryptpad/www/common/onlyoffice/dist
|
||||
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="blob block customize data datastore onlyoffice-conf" fetch_and_deploy_gh_release "cryptpad" "cryptpad/cryptpad" "tarball"
|
||||
|
||||
@@ -45,8 +47,30 @@ function update_script() {
|
||||
cd /opt/cryptpad
|
||||
$STD npm ci --allow-git=all
|
||||
$STD npm run install:components
|
||||
if [ -f "/opt/cryptpad/install-onlyoffice.sh" ]; then
|
||||
$STD bash /opt/cryptpad/install-onlyoffice.sh --accept-license
|
||||
if [[ -d /opt/cryptpad/www/common/onlyoffice/dist ]]; then
|
||||
$STD bash /opt/cryptpad/install-office.sh --accept-license
|
||||
fi
|
||||
if [[ ! -f /opt/cryptpad/config/infra.js ]]; then
|
||||
# 2026.10.0 reads the network settings from infra.js and no longer derives the sandbox origin
|
||||
LOCAL_IP="$LOCAL_IP" node - <<'EOF'
|
||||
const c = require("/opt/cryptpad/config/config.js");
|
||||
const i = require("/opt/cryptpad/config/infra.example.js");
|
||||
const port = c.httpPort || i.public.httpPort;
|
||||
const safePort = c.httpSafePort || port + 1;
|
||||
const origin = (c.httpUnsafeOrigin || `http://${process.env.LOCAL_IP}:${port}`).replace(/\/+$/, "");
|
||||
const sandbox = new URL(origin);
|
||||
sandbox.port = safePort;
|
||||
Object.assign(i.public, {
|
||||
origin,
|
||||
sandboxOrigin: (c.httpSafeOrigin || sandbox.origin).replace(/\/+$/, ""),
|
||||
httpHost: c.httpAddress || (c.httpUnsafeOrigin ? i.public.httpHost : "0.0.0.0"),
|
||||
httpPort: port,
|
||||
httpSafePort: safePort,
|
||||
externalWebsocketURL: c.externalWebsocketURL,
|
||||
fileHost: c.fileHost,
|
||||
});
|
||||
require("fs").writeFileSync("/opt/cryptpad/config/infra.js", "module.exports = " + JSON.stringify(i, null, 4) + ";\n");
|
||||
EOF
|
||||
fi
|
||||
$STD npm run build
|
||||
msg_ok "Updated CryptaPad"
|
||||
|
||||
@@ -38,57 +38,29 @@ function update_script() {
|
||||
$STD systemctl stop databasus
|
||||
msg_ok "Stopped Databasus"
|
||||
|
||||
create_backup /opt/databasus/.env
|
||||
|
||||
msg_info "Ensuring Database Clients"
|
||||
# Create PostgreSQL version symlinks for compatibility
|
||||
for v in 12 13 14 15 16 18; do
|
||||
ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v
|
||||
done
|
||||
# Install MongoDB Database Tools via direct .deb (no APT repo for Debian 13)
|
||||
if ! command -v mongodump &>/dev/null; then
|
||||
[[ "$(get_os_info id)" == "ubuntu" ]] && MONGO_DIST="ubuntu2204" || MONGO_DIST="debian12"
|
||||
MONGO_ARCH=$(arch_resolve "x86_64" "arm64")
|
||||
# MongoDB only publishes arm64 builds for Ubuntu
|
||||
[[ "$MONGO_ARCH" == "arm64" ]] && MONGO_DIST="ubuntu2204"
|
||||
fetch_and_deploy_from_url "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-${MONGO_DIST}-${MONGO_ARCH}-100.16.1.deb"
|
||||
fi
|
||||
ensure_dependencies mariadb-client
|
||||
mkdir -p /usr/local/mariadb-{10.6,12.1}/bin /usr/local/mysql-{5.7,8.0,8.4,9}/bin /usr/local/mongodb-database-tools/bin
|
||||
[[ -f /usr/bin/mongodump ]] && ln -sf /usr/bin/mongodump /usr/local/mongodb-database-tools/bin/mongodump
|
||||
[[ -f /usr/bin/mongorestore ]] && ln -sf /usr/bin/mongorestore /usr/local/mongodb-database-tools/bin/mongorestore
|
||||
# Create MariaDB and MySQL client symlinks for compatibility
|
||||
for dir in /usr/local/mariadb-{10.6,12.1}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mariadb-dump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mariadb"
|
||||
done
|
||||
for dir in /usr/local/mysql-{5.7,8.0,8.4,9}/bin; do
|
||||
ln -sf /usr/bin/mariadb-dump "$dir/mysqldump"
|
||||
ln -sf /usr/bin/mariadb "$dir/mysql"
|
||||
done
|
||||
msg_ok "Ensured Database Clients"
|
||||
|
||||
ensure_dependencies libedit2 libgssapi-krb5-2 libncurses6 libreadline8t64
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus"
|
||||
GO_VERSION="$(grep -m1 '^go ' /opt/databasus/backend/go.mod | awk '{print $2}')" setup_go
|
||||
|
||||
msg_info "Updating Databasus"
|
||||
RELEASE=$(cat ~/.databasus)
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
cd /opt/databasus/frontend
|
||||
|
||||
$STD corepack prepare pnpm@latest --activate
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm run build
|
||||
$STD env VITE_APP_VERSION="${RELEASE}" pnpm run build
|
||||
cd /opt/databasus/backend
|
||||
$STD go mod download
|
||||
$STD /root/go/bin/swag init -g cmd/main.go -o swagger
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$(arch_resolve) go build -o databasus ./cmd
|
||||
mv /opt/databasus/backend/databasus /opt/databasus/databasus
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH="$(arch_resolve)" go build -o /opt/databasus/databasus ./cmd/databasus
|
||||
mkdir -p /opt/databasus/agent-binaries
|
||||
for arch in amd64 arm64; do
|
||||
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -ldflags "-X main.Version=${RELEASE}" -o /opt/databasus/agent-binaries/databasus-verification-agent-linux-$arch ./cmd/verification-agent
|
||||
done
|
||||
mkdir -p /opt/databasus/ui/build
|
||||
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
|
||||
cp -r /opt/databasus/backend/migrations /opt/databasus/
|
||||
sed -i '/^APP_VERSION=/d' /.env
|
||||
echo "APP_VERSION=${RELEASE}" >>/.env
|
||||
msg_ok "Updated Databasus"
|
||||
|
||||
restore_backup
|
||||
chown -R postgres:postgres /opt/databasus
|
||||
|
||||
if ! grep -q "EnvironmentFile=/.env" /etc/systemd/system/databasus.service; then
|
||||
|
||||
@@ -33,6 +33,13 @@ function update_script() {
|
||||
|
||||
ensure_dependencies libgeos++-dev libxml2-dev libxslt-dev libjemalloc-dev
|
||||
|
||||
if [[ -f /etc/nginx/sites-available/dawarich.conf ]] && ! grep -q '\.mjs\$' /etc/nginx/sites-available/dawarich.conf; then
|
||||
msg_info "Fixing MapLibre delivery in Nginx"
|
||||
sed -i '/location ~ \^\/(assets|packs)\/ {/i\ location ~ \\.mjs$ {\n types { }\n default_type text/javascript;\n try_files $uri @rails;\n }\n' /etc/nginx/sites-available/dawarich.conf
|
||||
nginx_enable_site dawarich.conf
|
||||
msg_ok "Fixed MapLibre delivery in Nginx"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "dawarich" "Freika/dawarich"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop dawarich-web dawarich-worker
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/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/bambanah/deemix
|
||||
|
||||
APP="Deemix"
|
||||
var_tags="${var_tags:-music;downloader}"
|
||||
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:-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/deemix ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "deemix" "bambanah/deemix" "" "" "deemix-webui@"; then
|
||||
msg_info "Stopping Deemix"
|
||||
systemctl stop deemix
|
||||
msg_ok "Stopped Deemix"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "deemix" "bambanah/deemix" "tarball" "latest" "/opt/deemix" "" "deemix-webui@"
|
||||
NODE_VERSION="24" NODE_MODULE="pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([^"+]*\).*/\1/p' /opt/deemix/package.json)" setup_nodejs
|
||||
|
||||
msg_info "Building Deemix"
|
||||
cd /opt/deemix
|
||||
ELECTRON_SKIP_BINARY_DOWNLOAD=1 $STD pnpm install --frozen-lockfile
|
||||
$STD pnpm turbo build --filter=deemix-webui...
|
||||
msg_ok "Built Deemix"
|
||||
|
||||
msg_info "Starting Deemix"
|
||||
systemctl start deemix
|
||||
msg_ok "Started Deemix"
|
||||
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}:6595${CL}"
|
||||
@@ -36,28 +36,61 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop discourse
|
||||
msg_ok "Stopped Service"
|
||||
if grep -q 'X-Accel-Mapping' /etc/nginx/sites-available/discourse 2>/dev/null; then
|
||||
msg_info "Fixing stylesheet delivery in Nginx"
|
||||
sed -i '/X-Accel-Mapping/d' /etc/nginx/sites-available/discourse
|
||||
$STD systemctl reload nginx
|
||||
msg_ok "Fixed stylesheet delivery in Nginx"
|
||||
fi
|
||||
|
||||
create_backup /opt/discourse/.env
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop discourse discourse-sidekiq
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
msg_info "Updating Discourse"
|
||||
PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql
|
||||
cd /opt/discourse
|
||||
git pull origin main
|
||||
$STD bundle install --deployment --without test development
|
||||
$STD yarn install
|
||||
$STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
$STD bundle exec rails assets:precompile
|
||||
$STD bundle exec rails db:migrate
|
||||
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
export BUNDLE_SILENCE_ROOT_WARNING=1
|
||||
set -a
|
||||
source /opt/discourse/.env
|
||||
set +a
|
||||
$STD git pull origin main
|
||||
msg_ok "Updated Discourse"
|
||||
|
||||
restore_backup
|
||||
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 "Starting Service"
|
||||
systemctl start discourse
|
||||
msg_ok "Started Service"
|
||||
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
|
||||
export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH"
|
||||
hash -r
|
||||
cd /opt/discourse
|
||||
$STD bundle install -j"$(nproc)"
|
||||
$STD pnpm install
|
||||
msg_ok "Updated Dependencies"
|
||||
|
||||
msg_info "Running Database Migrations"
|
||||
$STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;"
|
||||
$STD bundle exec rails db:migrate
|
||||
msg_ok "Ran Database Migrations"
|
||||
|
||||
msg_info "Precompiling Assets"
|
||||
$STD bundle exec rails assets:precompile
|
||||
msg_ok "Precompiled Assets"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start discourse discourse-sidekiq
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ else
|
||||
var_disk="${var_disk:-4}"
|
||||
var_version="${var_version:-13}"
|
||||
fi
|
||||
# Only exported variables reach the install through lxc-attach.
|
||||
export var_docker_socket="${var_docker_socket:-}"
|
||||
export var_docker_compose="${var_docker_compose:-}"
|
||||
export var_docker_lxcfs="${var_docker_lxcfs:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
|
||||
@@ -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:-ubuntu}"
|
||||
var_version="${var_version:-24.04}"
|
||||
var_os="${var_os:-Debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-0}"
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/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/henriquesebastiao/downtify
|
||||
|
||||
APP="Downtify"
|
||||
var_tags="${var_tags:-music;downloader}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
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/downtify ]]; then
|
||||
msg_error "No Downtify Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "downtify" "henriquesebastiao/downtify"; then
|
||||
msg_info "Stopping Downtify"
|
||||
systemctl stop downtify
|
||||
msg_ok "Stopped Downtify"
|
||||
|
||||
PYTHON_VERSION="3.14" setup_uv
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
fetch_and_deploy_gh_release "deno" "denoland/deno" "prebuild" "latest" "/usr/local/bin" "deno-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.zip"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "downtify" "henriquesebastiao/downtify" "tarball"
|
||||
|
||||
msg_info "Building Downtify"
|
||||
cd /opt/downtify/frontend
|
||||
$STD npm ci
|
||||
$STD npm run build
|
||||
rm -rf /opt/downtify/frontend/node_modules
|
||||
cd /opt/downtify
|
||||
$STD uv sync --frozen --no-dev --python 3.14
|
||||
msg_ok "Built Downtify"
|
||||
|
||||
msg_info "Starting Downtify"
|
||||
systemctl start downtify
|
||||
msg_ok "Started Downtify"
|
||||
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}:8000${CL}"
|
||||
echo -e "${INFO}${YW}Sign in as admin / downtify and change the password right away${CL}"
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/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/dreeveapp/dreeve
|
||||
|
||||
APP="Dreeve"
|
||||
var_tags="${var_tags:-fitness;dashboard}"
|
||||
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}" # unset = ask the user; set yes/no only when verified
|
||||
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/dreeve ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "dreeve" "dreeveapp/dreeve"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop dreeve
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/dreeve/.env.local /opt/dreeve/Caddyfile
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "dreeve" "dreeveapp/dreeve" "tarball"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Installing PHP Dependencies (Patience)"
|
||||
cd /opt/dreeve
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
export APP_ENV=prod
|
||||
$STD /opt/frankenphp/frankenphp php-cli /usr/local/bin/composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
||||
msg_ok "Installed PHP Dependencies"
|
||||
|
||||
msg_info "Running Migrations"
|
||||
mkdir -p /opt/dreeve/var/{cache,log}
|
||||
set -a
|
||||
source /opt/dreeve/.env.local
|
||||
set +a
|
||||
$STD /opt/frankenphp/frankenphp php-cli bin/console app:db:migrate --no-interaction
|
||||
$STD /opt/frankenphp/frankenphp php-cli bin/console assets:install public --no-interaction
|
||||
rm -rf /opt/dreeve/var/cache/prod
|
||||
$STD /opt/frankenphp/frankenphp php-cli bin/console cache:warmup --env=prod
|
||||
msg_ok "Ran Migrations"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start dreeve
|
||||
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 "${GATEWAY}${BGN}http://${IP}:8080${CL}"
|
||||
echo -e "${INFO}${YW}Admin credentials: ~/dreeve.creds (inside the container)${CL}"
|
||||
echo -e "${INFO}${YW}Add your Strava API credentials to /opt/dreeve/.env.local first${CL}"
|
||||
@@ -91,6 +91,7 @@ User=root
|
||||
WorkingDirectory=/opt/droppedneedle/backend
|
||||
Environment=ROOT_APP_DIR=/opt/droppedneedle/backend
|
||||
Environment=PORT=8688
|
||||
Environment=COMMIT_TAG=v$(<~/.droppedneedle)
|
||||
# Environment=SLSKD_DOWNLOADS_PATH=<path-to-slskd-downloads>
|
||||
ExecStart=/opt/droppedneedle/venv/bin/python -m maintenance.automatic_upgrade --start-target
|
||||
Restart=on-failure
|
||||
@@ -106,6 +107,11 @@ EOF
|
||||
sed -i '\|=8688$|a# Environment=SLSKD_DOWNLOADS_PATH=<path-to-slskd-downloads>' /etc/systemd/system/droppedneedle.service
|
||||
fi
|
||||
sed -i 's|^ExecStart=.*|ExecStart=/opt/droppedneedle/venv/bin/python -m maintenance.automatic_upgrade --start-target|' /etc/systemd/system/droppedneedle.service
|
||||
if grep -q '^Environment=COMMIT_TAG=' /etc/systemd/system/droppedneedle.service; then
|
||||
sed -i "s|^Environment=COMMIT_TAG=.*|Environment=COMMIT_TAG=v$(<~/.droppedneedle)|" /etc/systemd/system/droppedneedle.service
|
||||
else
|
||||
sed -i "\|=8688$|a Environment=COMMIT_TAG=v$(<~/.droppedneedle)" /etc/systemd/system/droppedneedle.service
|
||||
fi
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/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/emdash-cms/emdash
|
||||
|
||||
APP="EmDash"
|
||||
var_tags="${var_tags:-cms;blog}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-3072}"
|
||||
var_disk="${var_disk:-6}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
export var_emdash_url="${var_emdash_url:-}"
|
||||
export var_emdash_template="${var_emdash_template:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/emdash ]]; then
|
||||
msg_error "No EmDash Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "emdash" "emdash-cms/emdash" "" "" "emdash@"; then
|
||||
msg_info "Stopping EmDash"
|
||||
systemctl stop emdash
|
||||
msg_ok "Stopped EmDash"
|
||||
|
||||
NODE_VERSION="24" NODE_MODULE="pnpm@11" setup_nodejs
|
||||
|
||||
msg_info "Updating EmDash"
|
||||
cd /opt/emdash
|
||||
set -a && source /opt/emdash_data/.env && set +a
|
||||
$STD pnpm add "${CHECK_UPDATE_RELEASE}"
|
||||
$STD pnpm build
|
||||
$STD pnpm store prune
|
||||
cat <<EOF >~/.emdash
|
||||
${CHECK_UPDATE_RELEASE}
|
||||
EOF
|
||||
msg_ok "Updated EmDash"
|
||||
|
||||
msg_info "Starting EmDash"
|
||||
systemctl start emdash
|
||||
msg_ok "Started EmDash"
|
||||
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}:4321${CL}"
|
||||
echo -e "${INFO}${YW}The admin at /_emdash/admin signs in with passkeys only, and browsers allow those on HTTPS with a domain or on localhost, never on an IP address.${CL}"
|
||||
echo -e "${INFO}${YW}Put EmDash behind a reverse proxy with a trusted certificate, set EMDASH_SITE_URL in /opt/emdash_data/.env to that address and rebuild (see the notes), then create the admin there.${CL}"
|
||||
@@ -30,14 +30,14 @@ function update_script() {
|
||||
msg_error "No ${APP} installation found!"
|
||||
exit 233
|
||||
fi
|
||||
if check_for_codeberg_release "endurain" "endurain-project/endurain"; then
|
||||
if check_for_gh_release "endurain" "endurain-project/endurain"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop endurain
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
create_backup /opt/endurain/.env /opt/endurain/frontend/dist/env.js
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_codeberg_release "endurain" "endurain-project/endurain" "tarball" "latest" "/opt/endurain"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "endurain" "endurain-project/endurain"
|
||||
|
||||
msg_info "Updating Endurain Frontend"
|
||||
cd /opt/endurain
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: Masked-Kunsiquat
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://www.espocrm.com/ | Github: https://github.com/espocrm/espocrm
|
||||
|
||||
APP="EspoCRM"
|
||||
var_tags="${var_tags:-crm;business}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
export var_admin_user="${var_admin_user:-}"
|
||||
export var_admin_pass="${var_admin_pass:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/espocrm ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "espo-crm" "espocrm/espocrm"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop nginx cron php8.4-fpm
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data custom client/custom" fetch_and_deploy_gh_release "espo-crm" "espocrm/espocrm" "prebuild" "latest" "/opt/espocrm" "EspoCRM-*.zip"
|
||||
|
||||
msg_info "Migrating EspoCRM"
|
||||
cd /opt/espocrm
|
||||
$STD php bin/command clear-cache
|
||||
if ! $STD php bin/command migrate; then
|
||||
chown -R www-data:www-data /opt/espocrm
|
||||
systemctl start php8.4-fpm
|
||||
systemctl start cron nginx
|
||||
msg_error "Migration failed; customizations may be incompatible. Check /opt/espocrm/data/logs, then run update again."
|
||||
exit 1
|
||||
fi
|
||||
chown -R www-data:www-data /opt/espocrm
|
||||
msg_ok "Migrated EspoCRM"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start php8.4-fpm
|
||||
systemctl start cron nginx
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
|
||||
@@ -35,7 +35,8 @@ function update_script() {
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop excalidash
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "excalidash" "ZimengXiong/ExcaliDash" "tarball"
|
||||
ln -sf /opt/excalidash_data/.env /opt/excalidash/backend/.env
|
||||
set -a && source /opt/excalidash_data/.env && set +a
|
||||
|
||||
@@ -102,6 +102,7 @@ function update_script() {
|
||||
fi
|
||||
msg_ok "Ensured ASP.NET Core Runtime"
|
||||
|
||||
rm -rf /opt/fileflows/{Server,FlowRunner,Libraries,Agent}
|
||||
fetch_and_deploy_from_url "https://fileflows.com/downloads/ff-latest.tar.xz" "/opt/fileflows"
|
||||
|
||||
msg_info "Starting Service"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mozilla-services/syncstorage-rs
|
||||
|
||||
APP="Firefox-Syncserver"
|
||||
var_tags="${var_tags:-sync;firefox}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-3072}"
|
||||
var_disk="${var_disk:-10}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
#var_arm64="${var_arm64:-yes}" # built from source, nothing x86-only, but not yet run on arm64
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /usr/local/bin/syncserver ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs"; then
|
||||
msg_info "Stopping Firefox Syncserver"
|
||||
systemctl stop firefox-syncserver
|
||||
msg_ok "Stopped Firefox Syncserver"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs" "tarball"
|
||||
setup_rust
|
||||
|
||||
msg_info "Building Firefox Syncserver (Patience)"
|
||||
cd /opt/firefox-syncserver
|
||||
$STD cargo build --release --locked --no-default-features --features=syncstorage-db/postgres --features=tokenserver-db/postgres --features=py_verifier
|
||||
cp target/release/syncserver /usr/local/bin/syncserver
|
||||
rm -rf target
|
||||
$STD uv venv --python /usr/bin/python3 /opt/firefox-syncserver/.venv
|
||||
$STD uv pip install --python /opt/firefox-syncserver/.venv/bin/python pyfxa tokenlib cryptography
|
||||
msg_ok "Built Firefox Syncserver"
|
||||
|
||||
msg_info "Starting Firefox Syncserver"
|
||||
systemctl start firefox-syncserver
|
||||
msg_ok "Started Firefox Syncserver"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}In Firefox, set identity.sync.tokenserver.uri in about:config to:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000/1.0/sync/1.5${CL}"
|
||||
@@ -35,6 +35,12 @@ function update_script() {
|
||||
msg_error "You must upgrade your LXC to Debian Trixie before updating."
|
||||
exit
|
||||
fi
|
||||
# FlareSolverr hands LANG to Chrome as Accept-Language, which needs a tag like en-US, not a locale (#17987)
|
||||
if ! grep -q '^Environment="LANG=' /etc/systemd/system/flaresolverr.service; then
|
||||
sed -i '/^Environment="CAPTCHA_SOLVER=/a Environment="LANG=en-US"' /etc/systemd/system/flaresolverr.service
|
||||
systemctl daemon-reload
|
||||
systemctl restart flaresolverr
|
||||
fi
|
||||
if check_for_gh_release "flaresolverr" "FlareSolverr/FlareSolverr"; then
|
||||
msg_info "Stopping service"
|
||||
systemctl stop flaresolverr
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/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) | Co-Author: 007hacky007
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/FoldingAtHome/fah-client-bastet
|
||||
|
||||
APP="FoldingAtHome"
|
||||
var_tags="${var_tags:-science;distributed-computing}"
|
||||
var_cpu="${var_cpu:-4}"
|
||||
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_gpu="${var_gpu:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
export var_fah_token="${var_fah_token:-}"
|
||||
export var_fah_machine_name="${var_fah_machine_name:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -f /usr/bin/fah-client ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
fetch_and_deploy_from_url "https://download.foldingathome.org/releases/public/fah-client/$(arch_resolve "debian-10-64bit" "debian-stable-arm64")/release/latest.deb"
|
||||
msg_ok "Folding@home is at $(dpkg-query -W -f='${Version}' fah-client)"
|
||||
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}Manage it from the Folding@home Web Control:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}https://app.foldingathome.org/${CL}"
|
||||
echo -e "${INFO}${YW}The client starts paused - press Fold once the machine shows up in your account${CL}"
|
||||
echo -e "${INFO}${YW}Account token and machine name are in /etc/fah-client/config.xml${CL}"
|
||||
@@ -10,7 +10,7 @@ APP="GeoPulse"
|
||||
var_tags="${var_tags:-location;tracking;gps}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_disk="${var_disk:-12}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
@@ -32,6 +32,16 @@ function update_script() {
|
||||
fi
|
||||
|
||||
if check_for_gh_release "geopulse-backend" "tess1o/geopulse"; then
|
||||
msg_info "Backing Up Database"
|
||||
mkdir -p /var/lib/geopulse/dumps
|
||||
DB_NAME="$(grep -oP '^GEOPULSE_POSTGRES_DB=\K.*' /etc/geopulse/geopulse.env 2>/dev/null)"
|
||||
DB_NAME="${DB_NAME:-geopulse}"
|
||||
if sudo -u postgres pg_dump -Fc "${DB_NAME}" >"/var/lib/geopulse/dumps/${DB_NAME}-pre-update-$(date +%Y%m%d%H%M%S).dump" 2>/dev/null; then
|
||||
msg_ok "Backed Up Database"
|
||||
else
|
||||
msg_warn "Database backup failed, continuing anyway"
|
||||
fi
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop geopulse-backend
|
||||
msg_ok "Stopped Service"
|
||||
@@ -51,7 +61,48 @@ function update_script() {
|
||||
fi
|
||||
|
||||
fetch_and_deploy_gh_release "geopulse-backend" "tess1o/geopulse" "singlefile" "latest" "/opt/geopulse/backend" "${BINARY_PATTERN}"
|
||||
fetch_and_deploy_gh_release "geopulse-frontend" "tess1o/geopulse" "prebuild" "latest" "/var/www/geopulse" "geopulse-frontend-*.tar.gz"
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="avatars" fetch_and_deploy_gh_release "geopulse-frontend" "tess1o/geopulse" "prebuild" "latest" "/var/www/geopulse" "geopulse-frontend-*.tar.gz"
|
||||
|
||||
if [[ -f /etc/geopulse/geopulse.env ]] && grep -q "GEOPULSE_JWT_" /etc/geopulse/geopulse.env 2>/dev/null; then
|
||||
msg_info "Removing Unused JWT Keys"
|
||||
sed -i '/^GEOPULSE_JWT_PRIVATE_KEY_LOCATION=/d;/^GEOPULSE_JWT_PUBLIC_KEY_LOCATION=/d' /etc/geopulse/geopulse.env
|
||||
rm -f /opt/geopulse/keys/jwt-private-key.pem /opt/geopulse/keys/jwt-public-key.pem
|
||||
msg_ok "Removed Unused JWT Keys"
|
||||
fi
|
||||
|
||||
if [[ -f /etc/nginx/sites-available/geopulse.conf ]] && ! grep -q '\.mjs\$' /etc/nginx/sites-available/geopulse.conf; then
|
||||
msg_info "Configuring Nginx for .mjs Assets"
|
||||
cp /etc/nginx/sites-available/geopulse.conf /etc/nginx/sites-available/geopulse.conf.pre-mjs.bak
|
||||
|
||||
ANCHOR_LINE="$(grep -n ' add_header Cache-Control "public, max-age=31536000";' /etc/nginx/sites-available/geopulse.conf.pre-mjs.bak | head -1 | cut -d: -f1)"
|
||||
|
||||
if [[ -n "$ANCHOR_LINE" ]]; then
|
||||
INSERT_LINE=$((ANCHOR_LINE + 1))
|
||||
{
|
||||
head -n "$INSERT_LINE" /etc/nginx/sites-available/geopulse.conf.pre-mjs.bak
|
||||
cat <<'MJSBLOCK'
|
||||
|
||||
location ~* ^/(?!osm/).*\.mjs$ {
|
||||
types {
|
||||
application/javascript mjs;
|
||||
}
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, max-age=31536000";
|
||||
}
|
||||
MJSBLOCK
|
||||
tail -n "+$((INSERT_LINE + 1))" /etc/nginx/sites-available/geopulse.conf.pre-mjs.bak
|
||||
} >/etc/nginx/sites-available/geopulse.conf
|
||||
fi
|
||||
|
||||
if [[ -n "$ANCHOR_LINE" ]] && grep -q '\.mjs\$' /etc/nginx/sites-available/geopulse.conf && nginx -t >/dev/null 2>&1; then
|
||||
systemctl reload nginx
|
||||
msg_ok "Configured Nginx for .mjs Assets"
|
||||
else
|
||||
cp /etc/nginx/sites-available/geopulse.conf.pre-mjs.bak /etc/nginx/sites-available/geopulse.conf
|
||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx
|
||||
msg_warn "Could not automatically add the .mjs nginx block (customized config?) - add it manually, see the GeoPulse 2.0.0 upgrade notes"
|
||||
fi
|
||||
fi
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start geopulse-backend
|
||||
|
||||
@@ -32,7 +32,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "gluetun" "qdm12/gluetun"; then
|
||||
if check_for_gh_release "gluetun" "passteque/gluetun"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop gluetun
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
@@ -45,15 +45,32 @@ function update_script() {
|
||||
export CGO_ENABLED=0
|
||||
$STD go mod download
|
||||
$STD go build -o /usr/local/bin/gotenberg \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v9/cmd.Version=$(cat ~/.gotenberg)'" \
|
||||
cmd/gotenberg/main.go
|
||||
msg_ok "Built gotenberg"
|
||||
|
||||
msg_info "Updating unoconverter"
|
||||
UNOCONVERTER_VERSION=$(get_latest_github_release "gotenberg/unoconverter" "false")
|
||||
download_file "https://raw.githubusercontent.com/gotenberg/unoconverter/${UNOCONVERTER_VERSION}/unoconv" /usr/local/bin/unoconverter
|
||||
chmod +x /usr/local/bin/unoconverter
|
||||
msg_ok "Updated unoconverter"
|
||||
LOK_VERSION="$(awk -F= '/^ARG LOK_VERSION=/{print $2}' /opt/gotenberg/build/Dockerfile)"
|
||||
fetch_and_deploy_gh_release "lok" "gotenberg/lok" "prebuild" "$LOK_VERSION" "/usr/local/bin" "lok_${LOK_VERSION#v}_linux_$(arch_resolve "amd64" "arm64").tar.gz"
|
||||
|
||||
msg_info "Migrating to Gotenberg 9"
|
||||
CODENAME=$(get_os_info codename)
|
||||
setup_deb822_repo \
|
||||
"debian-backports" \
|
||||
"file:///usr/share/keyrings/debian-archive-keyring.gpg" \
|
||||
"http://deb.debian.org/debian" \
|
||||
"${CODENAME}-backports" \
|
||||
"main"
|
||||
$STD apt install -y fonts-urw-base35
|
||||
$STD apt install -y -t "${CODENAME}-backports" \
|
||||
libreoffice-writer libreoffice-calc libreoffice-impress libreoffice-draw libreoffice-math
|
||||
$STD apt purge -y python3-uno python-is-python3 pdftk-java qpdf libimage-exiftool-perl
|
||||
$STD apt autoremove -y
|
||||
rm -rf /usr/local/bin/unoconverter /usr/local/bin/pdfcpu /opt/pdfcpu
|
||||
sed -i '/^Environment=\(UNOCONVERTER\|PDFTK\|QPDF\|EXIFTOOL\|PDFCPU\)_BIN_PATH=/d' /etc/systemd/system/gotenberg.service
|
||||
grep -q '^Environment=LOK_BIN_PATH=' /etc/systemd/system/gotenberg.service ||
|
||||
sed -i '/^Environment=LIBREOFFICE_BIN_PATH=/a Environment=LOK_BIN_PATH=/usr/local/bin/lok' /etc/systemd/system/gotenberg.service
|
||||
systemctl daemon-reload
|
||||
msg_ok "Migrated to Gotenberg 9"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start gotenberg
|
||||
|
||||
@@ -34,9 +34,43 @@ function update_script() {
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
NODE_VERSION="22" NODE_MODULE="corepack" setup_nodejs
|
||||
|
||||
if ! grep -q '^CELERY_CONFIG=' /opt/gramps-web/config/config.cfg; then
|
||||
msg_info "Setting up Celery Task Queue"
|
||||
ensure_dependencies redis-server
|
||||
cat <<EOF >>/opt/gramps-web/config/config.cfg
|
||||
CELERY_CONFIG={"broker_url":"redis://127.0.0.1:6379/0","result_backend":"redis://127.0.0.1:6379/0"}
|
||||
RATELIMIT_STORAGE_URI="redis://127.0.0.1:6379/1"
|
||||
EOF
|
||||
cat <<EOF >/etc/systemd/system/gramps-web-celery.service
|
||||
[Unit]
|
||||
Description=Gramps Web Celery Worker
|
||||
After=network.target redis-server.service
|
||||
Wants=redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/gramps-web-api
|
||||
Environment=GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg
|
||||
Environment=GRAMPSHOME=/opt/gramps-web/data
|
||||
Environment=GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb
|
||||
Environment=PATH=/opt/gramps-web/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/opt/gramps-web/venv/bin/celery -A gramps_webapi.celery worker --loglevel=INFO --concurrency=2
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable -q --now redis-server gramps-web-celery
|
||||
systemctl restart gramps-web
|
||||
msg_ok "Set up Celery Task Queue"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "gramps-web-api" "gramps-project/gramps-web-api"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop gramps-web
|
||||
systemctl stop gramps-web gramps-web-celery
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gramps-web-api" "gramps-project/gramps-web-api" "tarball" "latest" "/opt/gramps-web-api"
|
||||
@@ -71,7 +105,7 @@ function update_script() {
|
||||
msg_ok "Updated Gramps Addons"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start gramps-web
|
||||
systemctl start gramps-web gramps-web-celery
|
||||
msg_ok "Started Service"
|
||||
fi
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ function update_script() {
|
||||
msg_ok "Restored Database"
|
||||
fi
|
||||
|
||||
NODE_VERSION="26" setup_nodejs
|
||||
|
||||
msg_info "Updating Guardian"
|
||||
cd /opt/guardian/backend
|
||||
$STD npm ci
|
||||
|
||||
@@ -31,41 +31,62 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_warn "WARNING: This script will run an external installer from a third-party source (https://hermes-agent.nousresearch.com/)."
|
||||
msg_warn "The following code is NOT maintained or audited by our repository."
|
||||
msg_warn "If you have any doubts or concerns, please review the installer code before proceeding:"
|
||||
msg_custom "${TAB3}${GATEWAY}${BGN}${CL}" "\e[1;34m" "→ hermes update (https://hermes-agent.nousresearch.com/)"
|
||||
echo
|
||||
read -r -p "${TAB3}Do you want to continue? [y/N]: " CONFIRM
|
||||
if [[ ! "$CONFIRM" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||||
msg_error "Aborted by user. No changes have been made."
|
||||
exit 10
|
||||
confirm_external_installer "https://hermes-agent.nousresearch.com/" "hermes update (https://hermes-agent.nousresearch.com/)"
|
||||
|
||||
if [[ -f /etc/systemd/system/hermes-dashboard.service ]]; then
|
||||
msg_info "Moving Hermes to its own user"
|
||||
systemctl disable -q --now hermes-dashboard
|
||||
rm -f /etc/systemd/system/hermes-dashboard.service
|
||||
systemctl daemon-reload
|
||||
mapfile -t root_hermes_pids < <(ps -eo user=,pid=,args= | awk '$1 == "root" && /\/home\/hermes\/\.hermes\// && /hermes_cli[.]main/ { print $2 }')
|
||||
if ((${#root_hermes_pids[@]})); then
|
||||
kill -TERM "${root_hermes_pids[@]}" 2>/dev/null || true
|
||||
for pid in "${root_hermes_pids[@]}"; do
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 0.1
|
||||
done
|
||||
done
|
||||
fi
|
||||
ensure_dependencies libatomic1
|
||||
sed -i '/^HOME=/d' /etc/default/hermes
|
||||
grep -q '^GATEWAY_HEALTH_URL=' /etc/default/hermes || echo "GATEWAY_HEALTH_URL=http://127.0.0.1:8642/health" >>/etc/default/hermes
|
||||
mkdir -p /home/hermes/.config/systemd/user
|
||||
cat <<EOF >/home/hermes/.config/systemd/user/hermes-dashboard.service
|
||||
[Unit]
|
||||
Description=Hermes Agent Web Dashboard
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
UMask=0077
|
||||
WorkingDirectory=%h
|
||||
EnvironmentFile=/etc/default/hermes
|
||||
ExecStart=%h/.local/bin/hermes dashboard --host 127.0.0.1 --port 9119 --no-open
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
msg_ok "Moved Hermes to its own user"
|
||||
fi
|
||||
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop hermes-dashboard
|
||||
msg_ok "Stopped Services"
|
||||
# A writer racing chown makes files vanish between readdir and chown; one retry covers it.
|
||||
if ! chown -R hermes:hermes /home/hermes 2>/dev/null; then
|
||||
sleep 1
|
||||
chown -R hermes:hermes /home/hermes || msg_warn "Could not take ownership of everything under /home/hermes"
|
||||
fi
|
||||
$STD runuser -l hermes -c 'systemctl --user daemon-reload && systemctl --user enable hermes-dashboard'
|
||||
|
||||
msg_info "Updating Hermes Agent"
|
||||
$STD setsid --wait bash -c '
|
||||
$STD setsid --wait runuser -l hermes -c '
|
||||
set -a; source /etc/default/hermes; set +a
|
||||
/home/hermes/.local/bin/hermes update --yes
|
||||
hermes update --yes
|
||||
'
|
||||
# See https://github.com/community-scripts/ProxmoxVE/issues/17123
|
||||
mapfile -t root_gateway_pids < <(ps -eo user=,pid=,args= | awk '$1 == "root" && $0 ~ /\/home\/hermes\/\.hermes\/hermes-agent\/venv\/bin\/python -m hermes_cli\.main gateway run --replace$/ { print $2 }')
|
||||
if ((${#root_gateway_pids[@]})); then
|
||||
kill -TERM "${root_gateway_pids[@]}"
|
||||
for pid in "${root_gateway_pids[@]}"; do
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
sleep 0.1
|
||||
done
|
||||
done
|
||||
fi
|
||||
chown -R hermes:hermes /home/hermes
|
||||
msg_ok "Updated Hermes Agent"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start hermes-dashboard
|
||||
$STD runuser -l hermes -c 'systemctl --user start hermes-dashboard'
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
|
||||
@@ -85,6 +85,18 @@ EOF
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
# v2.3.0's Debian build ships a musl better-sqlite3 (homarr-labs/homarr#7097).
|
||||
mapfile -t MUSL_MODULES < <(find /opt/homarr -name better_sqlite3.node -exec grep -aqE "ld-musl|libc\.musl" {} \; -print)
|
||||
if ((${#MUSL_MODULES[@]})); then
|
||||
msg_info "Replacing musl better-sqlite3 build"
|
||||
BSQ_VERSION=$(jq -r .version /opt/homarr/node_modules/better-sqlite3/package.json)
|
||||
curl_with_retry "https://github.com/WiseLibs/better-sqlite3/releases/download/v${BSQ_VERSION}/better-sqlite3-v${BSQ_VERSION}-node-v$(node -p process.versions.modules)-linux-$(arch_resolve "x64" "arm64").tar.gz" /tmp/better-sqlite3.tar.gz
|
||||
tar -xzf /tmp/better-sqlite3.tar.gz -C /tmp build/Release/better_sqlite3.node
|
||||
for module in "${MUSL_MODULES[@]}"; do cp /tmp/build/Release/better_sqlite3.node "$module"; done
|
||||
rm -rf /tmp/better-sqlite3.tar.gz /tmp/build
|
||||
systemctl restart homarr
|
||||
msg_ok "Replaced musl better-sqlite3 build"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ function update_script() {
|
||||
docker pull "${CONTAINER_IMAGE}"
|
||||
LATEST_IMAGE="$(docker inspect --format "{{.Id}}" --type image "${CONTAINER_IMAGE}")"
|
||||
if [[ "${RUNNING_IMAGE}" != "${LATEST_IMAGE}" ]]; then
|
||||
pip install -U runlike
|
||||
pip install -U runlike --break-system-packages
|
||||
echo "Updating ${container} image ${CONTAINER_IMAGE}"
|
||||
DOCKER_COMMAND="$(runlike --use-volume-id "${container}")"
|
||||
docker rm --force "${container}"
|
||||
@@ -70,32 +70,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
if [ "$UPD" == "4" ]; then
|
||||
msg_info "Installing FileBrowser"
|
||||
RELEASE=$(curl -fsSL https://api.github.com/repos/filebrowser/filebrowser/releases/latest | grep -o '"tag_name": ".*"' | sed 's/"//g' | sed 's/tag_name: //g')
|
||||
$STD curl -fsSL https://github.com/filebrowser/filebrowser/releases/download/v2.23.0/linux-$(arch_resolve)-filebrowser.tar.gz | tar -xzv -C /usr/local/bin
|
||||
$STD filebrowser config init -a '0.0.0.0'
|
||||
$STD filebrowser config set -a '0.0.0.0'
|
||||
$STD filebrowser users add admin community-scripts.org --perm.admin
|
||||
msg_ok "Installed FileBrowser"
|
||||
|
||||
msg_info "Creating Service"
|
||||
service_path="/etc/systemd/system/filebrowser.service"
|
||||
echo "[Unit]
|
||||
Description=Filebrowser
|
||||
After=network-online.target
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/root/
|
||||
ExecStart=/usr/local/bin/filebrowser -r /
|
||||
[Install]
|
||||
WantedBy=default.target" >$service_path
|
||||
|
||||
$STD systemctl enable --now filebrowser
|
||||
msg_ok "Created Service"
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "FileBrowser should be reachable by going to the following URL.
|
||||
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
|
||||
exit
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="pnpm@latest" setup_nodejs
|
||||
NODE_VERSION="24" NODE_MODULE="pnpm@latest" setup_nodejs
|
||||
ensure_dependencies jq
|
||||
|
||||
if check_for_gh_release "homepage" "gethomepage/homepage"; then
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/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/EgalitarianMonkey/hometube
|
||||
|
||||
APP="HomeTube"
|
||||
var_tags="${var_tags:-media;youtube;downloader}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-20}"
|
||||
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/hometube ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Updating yt-dlp"
|
||||
$STD yt-dlp -U
|
||||
msg_ok "Updated yt-dlp"
|
||||
|
||||
fetch_and_deploy_gh_release "deno" "denoland/deno" "prebuild" "latest" "/usr/local/bin" "deno-$(arch_resolve "x86_64" "aarch64")-unknown-linux-gnu.zip"
|
||||
|
||||
if check_for_gh_release "hometube" "EgalitarianMonkey/hometube"; then
|
||||
msg_info "Stopping HomeTube"
|
||||
systemctl stop hometube
|
||||
msg_ok "Stopped HomeTube"
|
||||
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "hometube" "EgalitarianMonkey/hometube" "tarball"
|
||||
|
||||
msg_info "Updating HomeTube"
|
||||
cd /opt/hometube
|
||||
$STD uv sync --locked --no-dev --no-install-project --no-install-package pyarrow
|
||||
msg_ok "Updated HomeTube"
|
||||
|
||||
msg_info "Starting HomeTube"
|
||||
systemctl start hometube
|
||||
msg_ok "Started HomeTube"
|
||||
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}:8501${CL}"
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/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/plastic-labs/honcho
|
||||
|
||||
APP="Honcho"
|
||||
var_tags="${var_tags:-ai;memory;agents}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-6}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
export var_honcho_provider="${var_honcho_provider:-}"
|
||||
export var_honcho_api_key="${var_honcho_api_key:-}"
|
||||
export var_honcho_base_url="${var_honcho_base_url:-}"
|
||||
export var_honcho_model="${var_honcho_model:-}"
|
||||
export var_honcho_embedding_key="${var_honcho_embedding_key:-}"
|
||||
export var_honcho_embedding_model="${var_honcho_embedding_model:-}"
|
||||
export var_honcho_embedding_dims="${var_honcho_embedding_dims:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
if [[ -n "${var_honcho_provider}" && ! "${var_honcho_provider}" =~ ^(openai|anthropic|gemini)$ ]]; then
|
||||
msg_error "var_honcho_provider must be openai, anthropic or gemini"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/honcho ]]; then
|
||||
msg_error "No Honcho Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "honcho" "plastic-labs/honcho" "" "" "v"; then
|
||||
msg_info "Stopping Honcho"
|
||||
systemctl stop honcho-deriver honcho
|
||||
msg_ok "Stopped Honcho"
|
||||
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP=".env" fetch_and_deploy_gh_release "honcho" "plastic-labs/honcho" "tarball" "latest" "/opt/honcho" "" "v"
|
||||
|
||||
msg_info "Updating Honcho"
|
||||
cd /opt/honcho
|
||||
$STD uv sync --frozen --no-dev
|
||||
$STD .venv/bin/python scripts/provision_db.py
|
||||
msg_ok "Updated Honcho"
|
||||
|
||||
if systemctl is-enabled -q honcho; then
|
||||
msg_info "Starting Honcho"
|
||||
systemctl start honcho honcho-deriver
|
||||
msg_ok "Started Honcho"
|
||||
fi
|
||||
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 the API (interactive docs at /docs) using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
|
||||
echo -e "${INFO}${YW}Admin key, sent as Authorization: Bearer <key> (more with: cd /opt/honcho && .venv/bin/python scripts/generate_jwt.py --admin):${CL}"
|
||||
echo -e "${INFO}${YW}Without an LLM provider key Honcho stays stopped: set one in /opt/honcho_data/.env, then run: systemctl enable --now honcho honcho-deriver${CL}"
|
||||
@@ -65,26 +65,27 @@ EOF
|
||||
msg_ok "Installed/upgraded Testing repo packages"
|
||||
fi
|
||||
|
||||
if [[ ! -f /etc/apt/sources.list.d/mise.list ]]; then
|
||||
msg_info "Installing Mise"
|
||||
curl -fSs https://mise.jdx.dev/gpg-key.pub | tee /etc/apt/keyrings/mise-archive-keyring.pub 1>/dev/null
|
||||
echo "deb [signed-by=/etc/apt/keyrings/mise-archive-keyring.pub arch=$(arch_resolve)] https://mise.jdx.dev/deb stable main" >/etc/apt/sources.list.d/mise.list
|
||||
ensure_dependencies mise
|
||||
msg_ok "Installed Mise"
|
||||
if ! command -v mise >/dev/null 2>&1; then
|
||||
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}"
|
||||
fi
|
||||
|
||||
STAGING_DIR=/opt/staging
|
||||
BASE_DIR=${STAGING_DIR}/base-images
|
||||
SOURCE_DIR=${STAGING_DIR}/image-source
|
||||
cd /tmp
|
||||
RELEASE="v3.4.1"
|
||||
if [[ -f ~/.intel_version ]]; then
|
||||
curl_with_retry "https://raw.githubusercontent.com/immich-app/immich/refs/heads/main/machine-learning/Dockerfile" "Dockerfile"
|
||||
readarray -t INTEL_URLS < <(
|
||||
sed -n "/intel-[igc|opencl]/p" ./Dockerfile | awk '{print $3}'
|
||||
sed -n "/libigdgmm12/p" ./Dockerfile | awk '{print $3}'
|
||||
)
|
||||
INTEL_RELEASE="$(grep "intel-opencl-icd_" ./Dockerfile | awk -F '_' '{print $2}')"
|
||||
if [[ "$INTEL_RELEASE" != "$(cat ~/.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 ||
|
||||
curl_with_retry "${INTEL_SRC}/Dockerfile" "./intel-runtime"
|
||||
readarray -t INTEL_URLS < <(grep -oE 'https://github\.com/intel/[^[:space:]]+\.deb' ./intel-runtime)
|
||||
INTEL_RELEASE="$(grep -oE 'intel-opencl-icd_[^_]+' ./intel-runtime | cut -d_ -f2)" || true
|
||||
rm -f ./intel-runtime
|
||||
if [[ -z "$INTEL_RELEASE" ]]; then
|
||||
msg_warn "No Intel runtime found for Immich ${RELEASE}, keeping the installed one"
|
||||
elif [[ "$INTEL_RELEASE" != "$(cat ~/.intel_version)" ]]; then
|
||||
msg_info "Updating Intel OpenVINO dependencies"
|
||||
for url in "${INTEL_URLS[@]}"; do
|
||||
curl_with_retry "$url" "$(basename "$url")"
|
||||
@@ -96,25 +97,54 @@ EOF
|
||||
rm ./*.deb
|
||||
$STD apt-mark hold libigdgmm12
|
||||
dpkg-query -W -f='${Version}\n' intel-opencl-icd >~/.intel_version
|
||||
rm -f ./Dockerfile
|
||||
msg_ok "Updated Intel OpenVINO dependencies"
|
||||
fi
|
||||
fi
|
||||
if [[ -f ~/.immich_library_revisions ]]; then
|
||||
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
|
||||
$STD git pull
|
||||
for library in "${libraries[@]}"; do
|
||||
compile_"$library"
|
||||
done
|
||||
msg_ok "Image-processing libraries up to date"
|
||||
# Without the revision file the refresh was skipped and sharp failed on an old libvips (#17914).
|
||||
[[ -f ~/.immich_library_revisions ]] || msg_warn "No record of the compiled image-processing libraries, rebuilding all of them"
|
||||
for lib in imagemagick jpegli libheif libjxl libraw libvips; do
|
||||
grep -q "^${lib}:" ~/.immich_library_revisions 2>/dev/null || echo "${lib}: unknown" >>~/.immich_library_revisions
|
||||
done
|
||||
[[ -d "$BASE_DIR/.git" ]] || $STD git clone -b main https://github.com/immich-app/base-images "$BASE_DIR"
|
||||
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
|
||||
$STD git pull
|
||||
for library in "${libraries[@]}"; do
|
||||
compile_"$library"
|
||||
done
|
||||
msg_ok "Image-processing libraries up to date"
|
||||
|
||||
# 3.3.0 reads geodata/countryInfo.txt; the update before #17823 saved it into app/ instead.
|
||||
if [[ -d /opt/immich/geodata && ! -s /opt/immich/geodata/countryInfo.txt ]]; then
|
||||
msg_info "Restoring geodata/countryInfo.txt"
|
||||
if [[ -s /opt/immich/app/countryInfo.txt ]]; then
|
||||
mv /opt/immich/app/countryInfo.txt /opt/immich/geodata/countryInfo.txt
|
||||
else
|
||||
curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" /opt/immich/geodata/countryInfo.txt
|
||||
fi
|
||||
chown immich:immich /opt/immich/geodata/countryInfo.txt
|
||||
systemctl restart immich-web
|
||||
msg_ok "Restored geodata/countryInfo.txt"
|
||||
fi
|
||||
|
||||
RELEASE="v3.2.4"
|
||||
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
|
||||
if [[ $(cat ~/.immich) > "2.5.1" ]]; 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"
|
||||
|
||||
# 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"
|
||||
cd /opt/immich/app/bin
|
||||
$STD ./immich-admin enable-maintenance-mode || true
|
||||
@@ -122,6 +152,7 @@ EOF
|
||||
$STD cd -
|
||||
msg_ok "Enabled Maintenance Mode"
|
||||
fi
|
||||
[[ -x /opt/immich/app/bin/immich-admin ]] || export MAINT_MODE=1
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop immich-web
|
||||
systemctl stop immich-ml
|
||||
@@ -130,13 +161,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" "tensorchord/VectorChord" "${VCHORD_RELEASE}" "updated together with Immich after testing"; then
|
||||
if check_for_gh_release "VectorChord" "supervc-stack/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" "tensorchord/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${PG_VERSION}-vchord_*_$(arch_resolve).deb"
|
||||
fetch_and_deploy_gh_release "VectorChord" "supervc-stack/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;"
|
||||
@@ -145,14 +176,6 @@ EOF
|
||||
fi
|
||||
ensure_dependencies ccache gcc-13 g++-13
|
||||
|
||||
INSTALL_DIR="/opt/${APP}"
|
||||
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
|
||||
SRC_DIR="${INSTALL_DIR}/source"
|
||||
APP_DIR="${INSTALL_DIR}/app"
|
||||
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
|
||||
ML_DIR="${APP_DIR}/machine-learning"
|
||||
GEO_DIR="${INSTALL_DIR}/geodata"
|
||||
|
||||
[[ -f "$ML_DIR"/ml_start.sh ]] && cp "$ML_DIR"/ml_start.sh "$INSTALL_DIR"
|
||||
if grep -qs "set -a" "$APP_DIR"/bin/start.sh && grep -qs "warnings" "$APP_DIR"/bin/start.sh; then
|
||||
cp "$APP_DIR"/bin/start.sh "$INSTALL_DIR"
|
||||
@@ -174,8 +197,6 @@ EOF
|
||||
rm -rf "${APP_DIR:?}"/*
|
||||
)
|
||||
|
||||
setup_uv
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
|
||||
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
|
||||
export CI=1
|
||||
@@ -213,7 +234,8 @@ EOF
|
||||
# plugins
|
||||
cd "$SRC_DIR"
|
||||
export MISE_TRUSTED_CONFIG_PATHS="$SRC_DIR"/mise.toml
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg
|
||||
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,restic,npm:@openapitools/openapi-generator-cli
|
||||
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
|
||||
mise_ok=0
|
||||
for i in 1 2 3; do
|
||||
$STD mise install && {
|
||||
@@ -229,7 +251,8 @@ EOF
|
||||
# extism-js ships as a bare gzip-compressed single binary (.gz) that
|
||||
# fetch_and_deploy_gh_release cannot deploy; fetch + gunzip it directly.
|
||||
EXTISM_ARCH="$(arch_resolve x86_64 aarch64)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-${EXTISM_ARCH}-linux-v1.6.0.gz"
|
||||
EXTISM_VERSION="$(grep -oP '"github:extism/js-pdk"\s*=\s*"\K[^"]+' "$SRC_DIR"/mise.toml)"
|
||||
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/${EXTISM_VERSION}/extism-js-${EXTISM_ARCH}-linux-${EXTISM_VERSION}.gz"
|
||||
gunzip -f /tmp/extism-js.gz
|
||||
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
|
||||
rm -f /tmp/extism-js
|
||||
@@ -254,6 +277,27 @@ EOF
|
||||
cp ./packages/plugin-core/manifest.json "$PLUGIN_DIR"
|
||||
msg_ok "Updated Immich server, web, cli and plugins"
|
||||
|
||||
RESTIC_REF="$(grep -oP 'futo-org/restic:sha-\K[0-9a-f]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
RESTIC_PROXY_REF="$(grep -oP 'futo-org/backups/restic-proxy:\K[^@]+' "$SRC_DIR"/server/Dockerfile)"
|
||||
if [[ "$(cat ~/.immich_restic 2>/dev/null)" != "${RESTIC_REF} ${RESTIC_PROXY_REF}" || ! -x /usr/local/bin/restic ]]; then
|
||||
FUTO_DIR=/opt/staging/futo
|
||||
rm -rf "$FUTO_DIR"
|
||||
$STD git clone --filter=blob:none --no-checkout https://github.com/futo-org/restic "$FUTO_DIR"/restic
|
||||
$STD git -C "$FUTO_DIR"/restic checkout "$RESTIC_REF"
|
||||
$STD git clone --depth 1 --filter=blob:none --sparse -b "$RESTIC_PROXY_REF" https://github.com/futo-org/backups "$FUTO_DIR"/backups
|
||||
$STD git -C "$FUTO_DIR"/backups sparse-checkout set packages/restic-proxy
|
||||
GO_VERSION="$(awk '/^go /{print $2}' "$FUTO_DIR"/backups/packages/restic-proxy/go.mod)" setup_go
|
||||
msg_info "Building restic for FUTO Backups"
|
||||
cd "$FUTO_DIR"/restic
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic ./cmd/restic
|
||||
cd "$FUTO_DIR"/backups/packages/restic-proxy
|
||||
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic-proxy .
|
||||
cd "$SRC_DIR"
|
||||
rm -rf "$FUTO_DIR"
|
||||
echo "${RESTIC_REF} ${RESTIC_PROXY_REF}" >~/.immich_restic
|
||||
msg_ok "Built restic for FUTO Backups"
|
||||
fi
|
||||
|
||||
cd "$SRC_DIR"/machine-learning
|
||||
mkdir -p "$ML_DIR"
|
||||
# chown excluding upload dir contents (may be a mount with restricted permissions)
|
||||
@@ -272,13 +316,16 @@ 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 -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
|
||||
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
|
||||
$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
|
||||
[[ $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
|
||||
msg_warn "uv sync attempt $attempt failed, retrying one download at a time..." && sleep 10
|
||||
done
|
||||
patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-$(arch_resolve "x86_64" "aarch64")-linux-gnu.so"
|
||||
msg_ok "Updated Intel OpenVINO machine-learning"
|
||||
[[ "${ML_FAILED:-0}" == 1 ]] || patchelf --clear-execstack "${VIRTUAL_ENV}/lib/python3.13/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-313-$(arch_resolve "x86_64" "aarch64")-linux-gnu.so"
|
||||
[[ "${ML_FAILED:-0}" == 1 ]] || msg_ok "Updated Intel OpenVINO machine-learning"
|
||||
else
|
||||
ML_PYTHON="python3.11"
|
||||
ML_PYTHON="python3.13"
|
||||
msg_info "Pre-installing Python ${ML_PYTHON} for machine-learning"
|
||||
for attempt in $(seq 1 3); do
|
||||
$STD sudo --preserve-env=VIRTUAL_ENV -Pnu immich uv python install "${ML_PYTHON}" && break
|
||||
@@ -287,11 +334,15 @@ 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 -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
|
||||
[[ $attempt -lt 3 ]] && msg_warn "uv sync attempt $attempt failed, retrying..." && sleep 10
|
||||
$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
|
||||
[[ $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
|
||||
msg_warn "uv sync attempt $attempt failed, retrying one download at a time..." && sleep 10
|
||||
done
|
||||
msg_ok "Updated machine-learning"
|
||||
[[ "${ML_FAILED:-0}" == 1 ]] || msg_ok "Updated machine-learning"
|
||||
fi
|
||||
[[ "${ML_FAILED:-0}" == 1 ]] && msg_warn "uv sync failed three times, machine learning was not updated"
|
||||
cd "$SRC_DIR"
|
||||
cp -a machine-learning/{ann,immich_ml} "$ML_DIR"
|
||||
[[ -f "$INSTALL_DIR"/ml_start.sh ]] && mv "$INSTALL_DIR"/ml_start.sh "$ML_DIR"
|
||||
@@ -318,7 +369,7 @@ EOF
|
||||
grep -rl /usr/src | xargs -n1 sed -i "s|\/usr/src|$INSTALL_DIR|g"
|
||||
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
|
||||
[[ ! -f "$GEO_DIR/countryInfo.txt" ]] && curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
|
||||
[[ ! -f "$GEO_DIR/countryInfo.txt" ]] && curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "$GEO_DIR/countryInfo.txt"
|
||||
ln -s "${UPLOAD_DIR:-/opt/immich/upload}" "$APP_DIR"/upload
|
||||
ln -s "${UPLOAD_DIR:-/opt/immich/upload}" "$ML_DIR"/upload
|
||||
ln -sfn "$GEO_DIR" "$APP_DIR/geodata"
|
||||
@@ -332,33 +383,28 @@ EOF
|
||||
sed -i -e '$a\' "$INSTALL_DIR"/.env
|
||||
echo "IMMICH_HELMET_FILE=true" >>"$INSTALL_DIR"/.env
|
||||
fi
|
||||
if ! grep -q 'MODEL_REVISION' "$INSTALL_DIR"/.env; then
|
||||
sed -i -e '$a\' "$INSTALL_DIR"/.env
|
||||
echo "MACHINE_LEARNING_MODEL_REVISION=v2" >>"$INSTALL_DIR"/.env
|
||||
fi
|
||||
|
||||
if grep -q 'ExecStart=/usr/bin/node' /etc/systemd/system/immich-web.service; then
|
||||
sed -i '/^EnvironmentFile=/d' /etc/systemd/system/immich-web.service
|
||||
sed -i "s|^ExecStart=.*|ExecStart=${APP_DIR}/bin/start.sh|" /etc/systemd/system/immich-web.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
if grep -q "^ExecStart=${INSTALL_DIR}/ml_start.sh" /etc/systemd/system/immich-ml.service; then
|
||||
sed -i "s|^ExecStart=.*|ExecStart=${ML_DIR}/ml_start.sh|" /etc/systemd/system/immich-ml.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
# MickLesk temporary patch for HEIC thumbnail gen
|
||||
msg_info "Patching media.repository.js"
|
||||
MEDIA_REPO_JS="/opt/immich/app/dist/repositories/media.repository.js"
|
||||
if [[ -f "$MEDIA_REPO_JS" ]]; then
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
p = Path('/opt/immich/app/dist/repositories/media.repository.js')
|
||||
s = p.read_text()
|
||||
old = "(0, sharp_1.default)(input).metadata()"
|
||||
new = "(0, sharp_1.default)(input, { unlimited: true, limitInputPixels: false }).metadata()"
|
||||
if new in s:
|
||||
print('hotfix already there')
|
||||
elif old in s:
|
||||
p.write_text(s.replace(old, new, 1))
|
||||
print('hotfix applied')
|
||||
else:
|
||||
print('pattern not found, skipped')
|
||||
PY
|
||||
if grep -qF "(0, sharp_1.default)(input).metadata()" "$MEDIA_REPO_JS" 2>/dev/null; then
|
||||
msg_info "Patching media.repository.js"
|
||||
sed -i '0,/(0, sharp_1\.default)(input)\.metadata()/s//(0, sharp_1.default)(input, { unlimited: true, limitInputPixels: false }).metadata()/' "$MEDIA_REPO_JS"
|
||||
msg_ok "Patched media.repository.js"
|
||||
fi
|
||||
msg_ok "Patched media.repository.js"
|
||||
|
||||
# chown excluding upload dir contents (may be a mount with restricted permissions)
|
||||
chown immich:immich "$INSTALL_DIR"
|
||||
@@ -372,6 +418,15 @@ PY
|
||||
$STD cd -
|
||||
msg_ok "Disabled Maintenance Mode"
|
||||
fi
|
||||
# A crash loop before the update leaves the units rate-limited, and restart would refuse.
|
||||
systemctl reset-failed immich-ml immich-web 2>/dev/null || true
|
||||
if [[ "${ML_FAILED:-0}" == 1 ]]; then
|
||||
systemctl restart immich-web || true
|
||||
[[ -f /etc/systemd/system/immich-proxy.service ]] && systemctl restart immich-proxy
|
||||
echo "$PREV_IMMICH" >~/.immich
|
||||
msg_error "Immich runs, but without machine learning (see the uv output above). Run update again to retry."
|
||||
exit 1
|
||||
fi
|
||||
systemctl restart immich-ml immich-web
|
||||
[[ -f /etc/systemd/system/immich-proxy.service ]] && systemctl restart immich-proxy
|
||||
msg_ok "Updated successfully!"
|
||||
@@ -388,7 +443,7 @@ function compile_libjxl() {
|
||||
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
|
||||
cd "$SOURCE"
|
||||
$STD git reset --hard "$LIBJXL_REVISION"
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
|
||||
$STD git submodule update --init --recursive --depth 1 --recommend-shallow -- third_party
|
||||
mkdir build
|
||||
cd build
|
||||
$STD cmake \
|
||||
@@ -474,8 +529,7 @@ function compile_jpegli() {
|
||||
function compile_libheif() {
|
||||
SOURCE=${SOURCE_DIR}/libheif
|
||||
ensure_dependencies libaom-dev
|
||||
LIBHEIF_REVISION="ac1cb05c39008f01525c991ff8b88f84ddf70fd2"
|
||||
# : "${LIBHEIF_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libheif.json)}"
|
||||
LIBHEIF_REVISION="$(jq -cr '.revision' "$BASE_DIR"/server/sources/libheif.json)"
|
||||
if [[ "${update:-}" ]] || [[ "$LIBHEIF_REVISION" != "$(grep 'libheif' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
|
||||
msg_info "Recompiling libheif"
|
||||
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"
|
||||
@@ -550,7 +604,8 @@ function compile_imagemagick() {
|
||||
function compile_libvips() {
|
||||
SOURCE=$SOURCE_DIR/libvips
|
||||
LIBVIPS_REVISION="$(jq -cr '.revision' "$BASE_DIR"/server/sources/libvips.json)"
|
||||
if [[ "$LIBVIPS_REVISION" != "$(grep 'libvips' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
|
||||
if [[ "$LIBVIPS_REVISION" != "$(grep 'libvips' ~/.immich_library_revisions | awk '{print $2}')" ]] ||
|
||||
[[ "$(vips -l foreign 2>/dev/null)" != *jp2kload* ]]; then
|
||||
msg_info "Recompiling libvips"
|
||||
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"
|
||||
$STD git clone https://github.com/libvips/libvips.git "$SOURCE"
|
||||
|
||||
@@ -31,14 +31,36 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if ! grep -qE "^ID=(ubuntu)$" /etc/os-release; then
|
||||
msg_error "Unsupported OS. InvenTree requires Ubuntu (20.04/22.04/24.04)."
|
||||
if ! grep -qE "^ID=(ubuntu|debian)$" /etc/os-release; then
|
||||
msg_error "Unsupported OS. InvenTree requires Ubuntu or Debian."
|
||||
exit
|
||||
fi
|
||||
|
||||
# dl.packager.io is frozen at 1.5.1 and its SHA1-bound key fails on Debian 13;
|
||||
# go.packager.io only carries current builds for these releases
|
||||
if grep -qs "dl.packager.io" /etc/apt/sources.list.d/inventree*; then
|
||||
case "$(get_os_info id)/$(get_os_info version)" in
|
||||
ubuntu/24.04 | ubuntu/26.04 | debian/13)
|
||||
msg_info "Migrating InvenTree Repository"
|
||||
setup_deb822_repo \
|
||||
"inventree" \
|
||||
"https://go.packager.io/srv/deb/inventree/InvenTree/gpg-key.asc" \
|
||||
"https://go.packager.io/srv/deb/inventree/InvenTree/stable/$(get_os_info id)" \
|
||||
"$(get_os_info version)" \
|
||||
"main"
|
||||
msg_ok "Migrated InvenTree Repository"
|
||||
;;
|
||||
*)
|
||||
msg_warn "InvenTree only publishes updates for Ubuntu 24.04/26.04 and Debian 13; staying on the frozen repository"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
msg_info "Updating InvenTree"
|
||||
apt_update_safe
|
||||
$STD apt install --only-upgrade inventree -y
|
||||
# postinst loops forever unless its parent chain reaches PID 1 (not the case under pct exec)
|
||||
$STD systemd-run --wait --pipe --quiet --collect --setenv=DEBIAN_FRONTEND=noninteractive \
|
||||
apt-get install --only-upgrade -y inventree
|
||||
msg_ok "Updated InvenTree"
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
|
||||
@@ -31,6 +31,14 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ ! -f /etc/cron.d/invoiceshelf ]]; then
|
||||
msg_info "Adding the Laravel scheduler"
|
||||
cat <<'EOF' >/etc/cron.d/invoiceshelf
|
||||
* * * * * www-data cd /opt/invoiceshelf && php artisan schedule:run >> /dev/null 2>&1
|
||||
EOF
|
||||
msg_ok "Added the Laravel scheduler"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "invoiceshelf" "InvoiceShelf/InvoiceShelf"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop caddy
|
||||
|
||||
@@ -39,6 +39,10 @@ function update_script() {
|
||||
echo "Seerr is the new unified Jellyseerr and Overseerr."
|
||||
echo "More info: https://docs.seerr.dev/blog/seerr-release"
|
||||
echo
|
||||
if [[ "${PHS_SILENT:-0}" == "1" ]]; then
|
||||
msg_warn "Seerr migration detected - skipping update, requires an interactive run"
|
||||
exit 75
|
||||
fi
|
||||
read -rp "Do you want to migrate to Seerr now? (y/N): " MIGRATE
|
||||
echo
|
||||
if [[ ! "$MIGRATE" =~ ^[Yy]$ ]]; then
|
||||
@@ -96,6 +100,12 @@ EOF
|
||||
exit
|
||||
}
|
||||
|
||||
if [[ ! -d /opt/jellyseerr ]]; then
|
||||
msg_error "Jellyseerr is now Seerr and can no longer be installed with this script."
|
||||
msg_warn "Install Seerr instead: https://community-scripts.org/scripts/seerr"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
@@ -38,7 +38,7 @@ function update_script() {
|
||||
|
||||
create_backup /opt/jotty/.env /opt/jotty/data /opt/jotty/config
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="yarn" setup_nodejs
|
||||
NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "jotty" "fccview/jotty" "prebuild" "latest" "/opt/jotty" "jotty_*_prebuild.tar.gz"
|
||||
|
||||
restore_backup
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -91,7 +91,7 @@ EOF
|
||||
sed -i "s/^SERVER_VERSION=.*$/SERVER_VERSION=${CHECK_UPDATE_RELEASE#v}/" /etc/karakeep/karakeep.env
|
||||
# Node 24.19.x crashes better-sqlite3 on cleanup (karakeep-app/karakeep#2989); stay on 22 LTS until upstream reverts.
|
||||
MODULE_VERSION="$(jq -r '.packageManager | split("@")[1]' /opt/karakeep/package.json)"
|
||||
NODE_VERSION="22" NODE_MODULE="corepack,pnpm@${MODULE_VERSION}" setup_nodejs
|
||||
NODE_VERSION="24" NODE_MODULE="corepack,pnpm@${MODULE_VERSION}" setup_nodejs
|
||||
setup_meilisearch
|
||||
|
||||
msg_info "Updating Karakeep"
|
||||
|
||||
@@ -62,16 +62,7 @@ function update_script() {
|
||||
msg_info "Updating Kasm"
|
||||
cd /tmp
|
||||
|
||||
msg_warn "WARNING: This script will run an external installer from a third-party source (https://www.kasmweb.com/)."
|
||||
msg_warn "The following code is NOT maintained or audited by our repository."
|
||||
msg_warn "If you have any doubts or concerns, please review the installer code before proceeding:"
|
||||
msg_custom "${TAB3}${GATEWAY}${BGN}${CL}" "\e[1;34m" "→ upgrade.sh inside tar.gz $KASM_URL"
|
||||
echo
|
||||
read -r -p "${TAB3}Do you want to continue? [y/N]: " CONFIRM
|
||||
if [[ ! "$CONFIRM" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||||
msg_error "Aborted by user. No changes have been made."
|
||||
exit 10
|
||||
fi
|
||||
confirm_external_installer "https://www.kasmweb.com/" "upgrade.sh inside tar.gz $KASM_URL"
|
||||
curl_download "/tmp/kasm_release_${KASM_VERSION}.tar.gz" "$KASM_URL"
|
||||
tar -xf "kasm_release_${KASM_VERSION}.tar.gz"
|
||||
chmod +x /tmp/kasm_release/install.sh
|
||||
|
||||
@@ -64,6 +64,7 @@ function update_script() {
|
||||
$STD yarn prisma:deploy
|
||||
rm -rf ~/.cargo/registry ~/.cargo/git ~/.cargo/.package-cache
|
||||
rm -rf /root/.cache/yarn
|
||||
rm -rf /root/.yarn/berry/cache
|
||||
rm -rf /opt/linkwarden/.next/cache
|
||||
msg_ok "Updated Linkwarden"
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/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/Listenarrs/Listenarr
|
||||
|
||||
APP="Listenarr"
|
||||
var_tags="${var_tags:-arr;audiobook}"
|
||||
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:-no}" # upstream publishes a linux-x64 build only
|
||||
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/listenarr ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if GH_INCLUDE_PRERELEASE=1 check_for_gh_release "listenarr" "Listenarrs/Listenarr"; then
|
||||
msg_info "Stopping Listenarr"
|
||||
systemctl stop listenarr
|
||||
msg_ok "Stopped Listenarr"
|
||||
|
||||
GH_INCLUDE_PRERELEASE=1 CLEAN_INSTALL=1 fetch_and_deploy_gh_release "listenarr" "Listenarrs/Listenarr" "prebuild" "latest" "/opt/listenarr" "listenarr-*-linux-x64.zip"
|
||||
|
||||
msg_info "Starting Listenarr"
|
||||
systemctl start listenarr
|
||||
msg_ok "Started Listenarr"
|
||||
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}:4545${CL}"
|
||||