mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-11-12 18:25:28 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd38ed34e0 | ||
|
|
049f2dd151 | ||
|
|
57c299d068 | ||
|
|
959a86983c | ||
|
|
eb1005e9b5 | ||
|
|
6a33fc45ad |
No files matched your search
@@ -7,6 +7,11 @@
|
||||
*.func linguist-detectable=true
|
||||
*.install linguist-language=Shell
|
||||
|
||||
# ---------------------------------------
|
||||
# Exclude header art from stats
|
||||
# ---------------------------------------
|
||||
ct/headers/* linguist-documentation
|
||||
|
||||
# ---------------------------------------
|
||||
# Exclude documentation from stats
|
||||
# ---------------------------------------
|
||||
|
||||
+2
-18
@@ -58,9 +58,9 @@ body:
|
||||
|
||||
Before continuing, verify that the requested project meets the minimum requirements below.
|
||||
|
||||
**Projects with fewer than 1,000 stars, or younger than 6 months, are generally not eligible for a script request.**
|
||||
**Projects with fewer than 1,000 stars are generally not eligible for a script request.**
|
||||
|
||||
An automated check verifies the star count and project age for GitHub-hosted projects. If the project doesn't meet these requirements, fill in **Justification for an exception** below — without one, the request is closed automatically. With one, it's flagged for manual review instead. Exceptions are rare and require a clearly verifiable, significant public adoption signal.
|
||||
Exceptions are only considered where there is a clearly verifiable, significant public adoption signal.
|
||||
|
||||
- type: input
|
||||
id: repository-stars
|
||||
@@ -134,22 +134,6 @@ body:
|
||||
- label: I understand that requests outside the project scope may be closed without an extensive explanation.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: justification
|
||||
attributes:
|
||||
label: Justification for an exception
|
||||
description: >-
|
||||
Only needed if the project has fewer than 1,000 stars or is younger than 6 months (e.g. it has no GitHub
|
||||
presence, so stars don't apply). Explain the significant public adoption signal. Leave blank if the project
|
||||
meets the normal requirements — requests that don't meet the requirements and have no justification here are
|
||||
closed automatically.
|
||||
placeholder: |
|
||||
Example:
|
||||
No public GitHub repository, but the project has 50k+ active installs reported by its own telemetry
|
||||
dashboard and is covered by several established self-hosting publications.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
||||
Generated
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Generated
-15
@@ -1,15 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 997 B |
Generated
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
Generated
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Generated
+19
-3
@@ -10,7 +10,9 @@
|
||||
],
|
||||
"excludeGlobs": [
|
||||
"**/headers/**",
|
||||
"tools/headers/**"
|
||||
"ct/headers/**",
|
||||
"tools/headers/**",
|
||||
"vm/headers/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -46,7 +48,9 @@
|
||||
],
|
||||
"excludeGlobs": [
|
||||
"**/headers/**",
|
||||
"tools/headers/**"
|
||||
"ct/headers/**",
|
||||
"tools/headers/**",
|
||||
"vm/headers/**"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -77,6 +81,17 @@
|
||||
"excludeGlobs": []
|
||||
}
|
||||
],
|
||||
"core": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"misc/*.func"
|
||||
],
|
||||
"excludeGlobs": [
|
||||
"misc/api.func"
|
||||
]
|
||||
}
|
||||
],
|
||||
"documentation": [
|
||||
{
|
||||
"fileStatus": null,
|
||||
@@ -103,7 +118,8 @@
|
||||
{
|
||||
"fileStatus": null,
|
||||
"includeGlobs": [
|
||||
"api/**"
|
||||
"api/**",
|
||||
"misc/api.func"
|
||||
],
|
||||
"excludeGlobs": []
|
||||
}
|
||||
|
||||
Generated
-230
@@ -1,233 +1,3 @@
|
||||
## 2026-09-30
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Shoko ([#17617](https://github.com/community-scripts/ProxmoxVE/pull/17617))
|
||||
- SIlo ([#17614](https://github.com/community-scripts/ProxmoxVE/pull/17614))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(blocky): restart on failure and wait for network-online [@haplo](https://github.com/haplo) ([#17601](https://github.com/community-scripts/ProxmoxVE/pull/17601))
|
||||
- SparkyFitness: update sed replacement [@tomfrenzel](https://github.com/tomfrenzel) ([#17604](https://github.com/community-scripts/ProxmoxVE/pull/17604))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- vm: autologin on the serial console for images without cloud-init [@MickLesk](https://github.com/MickLesk) ([core#80](https://github.com/community-scripts/core/pull/80))
|
||||
- download_gpg_key: make dearmored keyrings world-readable [@MickLesk](https://github.com/MickLesk) ([core#79](https://github.com/community-scripts/core/pull/79))
|
||||
|
||||
## 2026-09-29
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Anki Sync Server ([#17416](https://github.com/community-scripts/ProxmoxVE/pull/17416))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(kaneo): build with Vite+ instead of Turbo [@tinsever](https://github.com/tinsever) ([#17599](https://github.com/community-scripts/ProxmoxVE/pull/17599))
|
||||
- tianji: raise the Node heap for the static build on install [@MickLesk](https://github.com/MickLesk) ([#17582](https://github.com/community-scripts/ProxmoxVE/pull/17582))
|
||||
- fix(romm): snapshot Redis hourly like the upstream image [@DenislavDenev](https://github.com/DenislavDenev) ([#17562](https://github.com/community-scripts/ProxmoxVE/pull/17562))
|
||||
- checkmate: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17554](https://github.com/community-scripts/ProxmoxVE/pull/17554))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Drop the scripts base pin from every ct/ script [@MickLesk](https://github.com/MickLesk) ([#17585](https://github.com/community-scripts/ProxmoxVE/pull/17585))
|
||||
- odoo: move to Debian 13 and stay on the installed major on update [@MickLesk](https://github.com/MickLesk) ([#17581](https://github.com/community-scripts/ProxmoxVE/pull/17581))
|
||||
- manyfold: use upstream f3d for headless renders on amd64 [@MickLesk](https://github.com/MickLesk) ([#17583](https://github.com/community-scripts/ProxmoxVE/pull/17583))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Default the scripts base to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([core#76](https://github.com/community-scripts/core/pull/76))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- all-templates: handle the arch column of pveam available [@MickLesk](https://github.com/MickLesk) ([#17579](https://github.com/community-scripts/ProxmoxVE/pull/17579))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Skip automated core pull requests in the changelog [@MickLesk](https://github.com/MickLesk) ([#17589](https://github.com/community-scripts/ProxmoxVE/pull/17589))
|
||||
- Post a test command for vm/, tools/ and turnkey/ changes too [@MickLesk](https://github.com/MickLesk) ([#17587](https://github.com/community-scripts/ProxmoxVE/pull/17587))
|
||||
- Remove the PocketBase AI bot [@MickLesk](https://github.com/MickLesk) ([#17591](https://github.com/community-scripts/ProxmoxVE/pull/17591))
|
||||
|
||||
## 2026-09-28
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- Immich: Pin version to 3.2.4 [@vhsdream](https://github.com/vhsdream) ([#17564](https://github.com/community-scripts/ProxmoxVE/pull/17564))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(autocaliweb): wire .env into all service units, not just autocali… [@bobartlett](https://github.com/bobartlett) ([#17561](https://github.com/community-scripts/ProxmoxVE/pull/17561))
|
||||
- Zipline: Change cp command [@Mraedis](https://github.com/Mraedis) ([#17559](https://github.com/community-scripts/ProxmoxVE/pull/17559))
|
||||
- Borg-UI: start through upstream's start.sh so migrations run [@MickLesk](https://github.com/MickLesk) ([#17563](https://github.com/community-scripts/ProxmoxVE/pull/17563))
|
||||
- linkding: serve favicons and preview images [@MickLesk](https://github.com/MickLesk) ([#17557](https://github.com/community-scripts/ProxmoxVE/pull/17557))
|
||||
- SparkyFitness: Fill in the nginx resolver 1.7.3 added [@MickLesk](https://github.com/MickLesk) ([#17556](https://github.com/community-scripts/ProxmoxVE/pull/17556))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Plane: Replace MinIO installation with Silo [@MickLesk](https://github.com/MickLesk) ([#17347](https://github.com/community-scripts/ProxmoxVE/pull/17347))
|
||||
|
||||
## 2026-09-27
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(aurral): bump to Node 26 and bypass strict engine pin [@JodyVanden](https://github.com/JodyVanden) ([#17543](https://github.com/community-scripts/ProxmoxVE/pull/17543))
|
||||
- Tracearr: fetch map tiles on install/update [@durzo](https://github.com/durzo) ([#17544](https://github.com/community-scripts/ProxmoxVE/pull/17544))
|
||||
- fix(autocaliweb): define INSTALL_DIR before first use [@bobartlett](https://github.com/bobartlett) ([#17545](https://github.com/community-scripts/ProxmoxVE/pull/17545))
|
||||
|
||||
## 2026-09-26
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Manyfold: make /opt/manyfold_data writable for the manyfold user [@sergstepanenko](https://github.com/sergstepanenko) ([#17534](https://github.com/community-scripts/ProxmoxVE/pull/17534))
|
||||
- steamcmd: update itself before installing Satisfactory [@MickLesk](https://github.com/MickLesk) ([#17526](https://github.com/community-scripts/ProxmoxVE/pull/17526))
|
||||
- AudioMuse-AI: build noavx2 environment on Python 3.11 [@MickLesk](https://github.com/MickLesk) ([#17528](https://github.com/community-scripts/ProxmoxVE/pull/17528))
|
||||
- LobeHub: pin lobehub/ui at 5.49 while LobeHub's stable release needs it [@MickLesk](https://github.com/MickLesk) ([#17531](https://github.com/community-scripts/ProxmoxVE/pull/17531))
|
||||
- Keep Trilium updates on the server releases [@MickLesk](https://github.com/MickLesk) ([#17525](https://github.com/community-scripts/ProxmoxVE/pull/17525))
|
||||
- NginxProxyManager: generate admin config and repair certbot's venv on update [@MickLesk](https://github.com/MickLesk) ([#17523](https://github.com/community-scripts/ProxmoxVE/pull/17523))
|
||||
- Komodo: add KOMODO_HOST | fix broken spinner [@MickLesk](https://github.com/MickLesk) ([#17481](https://github.com/community-scripts/ProxmoxVE/pull/17481))
|
||||
- romm: run rq cron and a scans worker for RomM 5.3 [@davidbb](https://github.com/davidbb) ([#17502](https://github.com/community-scripts/ProxmoxVE/pull/17502))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Semaphore: fix BoltDB migration by pinning 2.18.30 [@MickLesk](https://github.com/MickLesk) ([#17439](https://github.com/community-scripts/ProxmoxVE/pull/17439))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Keep a dash-led bridge comment out of whiptail's options [@MickLesk](https://github.com/MickLesk) ([core#74](https://github.com/community-scripts/core/pull/74))
|
||||
|
||||
## 2026-09-25
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- RustFS ([#17499](https://github.com/community-scripts/ProxmoxVE/pull/17499))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Ignis: take the Obsidian version from release tag [@MickLesk](https://github.com/MickLesk) ([#17503](https://github.com/community-scripts/ProxmoxVE/pull/17503))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Umbrel OS VM [@MickLesk](https://github.com/MickLesk) ([#17505](https://github.com/community-scripts/ProxmoxVE/pull/17505))
|
||||
- Docker-LXC: remove Portainer installation from install [@MickLesk](https://github.com/MickLesk) ([#17493](https://github.com/community-scripts/ProxmoxVE/pull/17493))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Incus: make prompts visible for end-user [@MickLesk](https://github.com/MickLesk) ([core#73](https://github.com/community-scripts/core/pull/73))
|
||||
- incus: do not let an install script block on an invisible prompt [@MickLesk](https://github.com/MickLesk) ([core#71](https://github.com/community-scripts/core/pull/71))
|
||||
- tel: report progress from installs only, not updates [@MickLesk](https://github.com/MickLesk) ([core#72](https://github.com/community-scripts/core/pull/72))
|
||||
|
||||
## 2026-09-24
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Declare the RomM filesystem structure required since 5.3.0 [@MickLesk](https://github.com/MickLesk) ([#17480](https://github.com/community-scripts/ProxmoxVE/pull/17480))
|
||||
- general: use apt_update_safe instead of aborting on a failed apt update [@MickLesk](https://github.com/MickLesk) ([#17462](https://github.com/community-scripts/ProxmoxVE/pull/17462))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Journiv: Serve Node Frontend and add HTTP Auth .env [@MickLesk](https://github.com/MickLesk) ([#17479](https://github.com/community-scripts/ProxmoxVE/pull/17479))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- core: accept an off-subnet gateway for /31 and /32 [@MickLesk](https://github.com/MickLesk) ([core#70](https://github.com/community-scripts/core/pull/70))
|
||||
|
||||
## 2026-09-23
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(signoz): install the histogramQuantile ClickHouse function [@maksimtech](https://github.com/maksimtech) ([#17386](https://github.com/community-scripts/ProxmoxVE/pull/17386))
|
||||
- omniroute: bump to 4 GB for the npm install [@MickLesk](https://github.com/MickLesk) ([#17461](https://github.com/community-scripts/ProxmoxVE/pull/17461))
|
||||
- Run Borg-UI with a single gunicorn worker [@MickLesk](https://github.com/MickLesk) ([#17445](https://github.com/community-scripts/ProxmoxVE/pull/17445))
|
||||
- immich: keep geodata linked and the build deps present on update [@MickLesk](https://github.com/MickLesk) ([#17438](https://github.com/community-scripts/ProxmoxVE/pull/17438))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Refactor: Scanopy [@MickLesk](https://github.com/MickLesk) ([#16797](https://github.com/community-scripts/ProxmoxVE/pull/16797))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- stop a partial find from aborting the caller under pipefail [@MickLesk](https://github.com/MickLesk) ([core#68](https://github.com/community-scripts/core/pull/68))
|
||||
- core: give RHEL-family containers an ostype PVE accepts [@MickLesk](https://github.com/MickLesk) ([core#67](https://github.com/community-scripts/core/pull/67))
|
||||
- core: do not abort on a failed apt update [@MickLesk](https://github.com/MickLesk) ([core#66](https://github.com/community-scripts/core/pull/66))
|
||||
- VM's: report a finished VM as done, not aborted [@MickLesk](https://github.com/MickLesk) ([core#65](https://github.com/community-scripts/core/pull/65))
|
||||
- VM-Core: decide console autologin from the image, not USE_CLOUD_INIT [@MickLesk](https://github.com/MickLesk) ([core#63](https://github.com/community-scripts/core/pull/63))
|
||||
- VM-Core: report failed image steps and restore tty1 autologin without cloud-init [@MickLesk](https://github.com/MickLesk) ([core#62](https://github.com/community-scripts/core/pull/62))
|
||||
- tools: raise pnpm/yarn fetch timeout and lower network concurrency [@MickLesk](https://github.com/MickLesk) ([core#59](https://github.com/community-scripts/core/pull/59))
|
||||
- VM-Core: stop discarding cloud-init error messages [@MickLesk](https://github.com/MickLesk) ([core#60](https://github.com/community-scripts/core/pull/60))
|
||||
|
||||
## 2026-09-22
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(invoiceshelf): re-link storage during update [@owenvoke](https://github.com/owenvoke) ([#17431](https://github.com/community-scripts/ProxmoxVE/pull/17431))
|
||||
- fix(alpine-it-tools): write the version file after a successful update [@Houtek](https://github.com/Houtek) ([#17421](https://github.com/community-scripts/ProxmoxVE/pull/17421))
|
||||
- Umami: Align pnpm with engines.pnpm on update [@ReneNulschDE](https://github.com/ReneNulschDE) ([#17404](https://github.com/community-scripts/ProxmoxVE/pull/17404))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- several scripts: let uv see the project before syncing it | refactor some scripts that use uv [@MickLesk](https://github.com/MickLesk) ([#17436](https://github.com/community-scripts/ProxmoxVE/pull/17436))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- setup_uv: honour a project's required-version pin [@MickLesk](https://github.com/MickLesk) ([core#58](https://github.com/community-scripts/core/pull/58))
|
||||
- fix(motd): repair legacy 00_lxc-details.sh header and dead color placeholders [@BadFlo](https://github.com/BadFlo) ([core#53](https://github.com/community-scripts/core/pull/53))
|
||||
- meilisearch: take the raw binary, not the .deb [@MickLesk](https://github.com/MickLesk) ([core#57](https://github.com/community-scripts/core/pull/57))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- github: Stop generating header filenames with a newline in them [@MickLesk](https://github.com/MickLesk) ([#17435](https://github.com/community-scripts/ProxmoxVE/pull/17435))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- chore(ct): sync cloudflare-ddns defaults with PocketBase [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17446](https://github.com/community-scripts/ProxmoxVE/pull/17446))
|
||||
|
||||
## 2026-09-21
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- AudioMuse-AI ([#17415](https://github.com/community-scripts/ProxmoxVE/pull/17415))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(portabase): meet the default admin password rules [@maksimtech](https://github.com/maksimtech) ([#17384](https://github.com/community-scripts/ProxmoxVE/pull/17384))
|
||||
- truenas-vm: strip the G before the storage allocation [@MickLesk](https://github.com/MickLesk) ([#17407](https://github.com/community-scripts/ProxmoxVE/pull/17407))
|
||||
- Librenms - Fix local snmpd community string setup [@dopch](https://github.com/dopch) ([#17363](https://github.com/community-scripts/ProxmoxVE/pull/17363))
|
||||
- fix(iventoy): preserve release data and migrate legacy service launchers [@Msprg](https://github.com/Msprg) ([#17161](https://github.com/community-scripts/ProxmoxVE/pull/17161))
|
||||
- xyops: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17398](https://github.com/community-scripts/ProxmoxVE/pull/17398))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Generate app headers [@github-actions[bot]](https://github.com/github-actions[bot]) ([core#56](https://github.com/community-scripts/core/pull/56))
|
||||
- github-api: take the release the maintainer marked as latest [@MickLesk](https://github.com/MickLesk) ([core#55](https://github.com/community-scripts/core/pull/55))
|
||||
- general: prune old Rust toolchains [@MickLesk](https://github.com/MickLesk) ([core#52](https://github.com/community-scripts/core/pull/52))
|
||||
- Ask when the preselected storage cannot hold the content [@MickLesk](https://github.com/MickLesk) ([core#51](https://github.com/community-scripts/core/pull/51))
|
||||
|
||||
## 2026-09-20
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Your-Spotify ([#17376](https://github.com/community-scripts/ProxmoxVE/pull/17376))
|
||||
|
||||
## 2026-09-19
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
Generated
-802
@@ -1,802 +0,0 @@
|
||||
## 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
|
||||
|
||||
- Keeper.sh ([#17659](https://github.com/community-scripts/ProxmoxVE/pull/17659))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- hermesagent: download the installer before running it [@MickLesk](https://github.com/MickLesk) ([#17666](https://github.com/community-scripts/ProxmoxVE/pull/17666))
|
||||
- frigate: raise the Node heap for the web build [@MickLesk](https://github.com/MickLesk) ([#17667](https://github.com/community-scripts/ProxmoxVE/pull/17667))
|
||||
- Tracearr: workaround for failing map tiles download and data preservation [@durzo](https://github.com/durzo) ([#17661](https://github.com/community-scripts/ProxmoxVE/pull/17661))
|
||||
- fix(nginxproxymanager): clean yarn cache after builds to prevent disk growth [@PsycoStea](https://github.com/PsycoStea) ([#17653](https://github.com/community-scripts/ProxmoxVE/pull/17653))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Several Scripts: refactor msg_ blocks, generate passwords with helper, add CLEAN_INSTALL_KEEP [@MickLesk](https://github.com/MickLesk) ([#17655](https://github.com/community-scripts/ProxmoxVE/pull/17655))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- detect_repo_source: read the license header and fall back to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([core#85](https://github.com/community-scripts/core/pull/85))
|
||||
|
||||
## 2026-10-02
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- zerobyte: upgrade Bun on update [@MickLesk](https://github.com/MickLesk) ([#17646](https://github.com/community-scripts/ProxmoxVE/pull/17646))
|
||||
- sure: keep Active Storage uploads across updates [@MickLesk](https://github.com/MickLesk) ([#17648](https://github.com/community-scripts/ProxmoxVE/pull/17648))
|
||||
- twenty: run upstreams upgrade command instead of the removed setup-db.ts [@MickLesk](https://github.com/MickLesk) ([#17647](https://github.com/community-scripts/ProxmoxVE/pull/17647))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- NPM: Refactor / Fix npm openresty user and certbot version [@MickLesk](https://github.com/MickLesk) ([#17649](https://github.com/community-scripts/ProxmoxVE/pull/17649))
|
||||
- several scripts: use the release helpers instead of hand-rolled version checks and downloads [@MickLesk](https://github.com/MickLesk) ([#17625](https://github.com/community-scripts/ProxmoxVE/pull/17625))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- QoL: message blocks, clean-install keep list, password helper, network limits [@MickLesk](https://github.com/MickLesk) ([core#61](https://github.com/community-scripts/core/pull/61))
|
||||
|
||||
## 2026-10-01
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- romm: keep the asset links in sync with ROMM_BASE_PATH [@MickLesk](https://github.com/MickLesk) ([#17610](https://github.com/community-scripts/ProxmoxVE/pull/17610))
|
||||
- teslamate: build from the elixir directory since 4.3.0 [@MickLesk](https://github.com/MickLesk) ([#17612](https://github.com/community-scripts/ProxmoxVE/pull/17612))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- feat(glance): add alpine os [@skilletfun](https://github.com/skilletfun) ([#17605](https://github.com/community-scripts/ProxmoxVE/pull/17605))
|
||||
- feat(blocky): add alpine os [@skilletfun](https://github.com/skilletfun) ([#17637](https://github.com/community-scripts/ProxmoxVE/pull/17637))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- several scripts: apt instead of apt-get, cleanup_lxc, nginx_enable_site, drop needless daemon-reloads [@MickLesk](https://github.com/MickLesk) ([#17624](https://github.com/community-scripts/ProxmoxVE/pull/17624))
|
||||
- several scripts: use setup_postgresql_db / setup_mariadb_db instead of hand-written SQL [@MickLesk](https://github.com/MickLesk) ([#17622](https://github.com/community-scripts/ProxmoxVE/pull/17622))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Offer to raise the kernel key limits instead of only naming them [@MickLesk](https://github.com/MickLesk) ([core#81](https://github.com/community-scripts/core/pull/81))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- tools, turnkey: load core instead of the legacy misc functions [@MickLesk](https://github.com/MickLesk) ([#17621](https://github.com/community-scripts/ProxmoxVE/pull/17621))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- chore: drop the local header generation; headers come from core [@MickLesk](https://github.com/MickLesk) ([#17623](https://github.com/community-scripts/ProxmoxVE/pull/17623))
|
||||
Generated
-187
@@ -1,187 +0,0 @@
|
||||
## 2026-11-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
|
||||
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
|
||||
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
|
||||
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
|
||||
|
||||
## 2026-11-06
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- UmbrelOS-VM: report runs as ProxmoxVE, not DevScripts [@MickLesk](https://github.com/MickLesk) ([#18497](https://github.com/community-scripts/ProxmoxVE/pull/18497))
|
||||
- Caddy: skip the xCaddy test build below 2 GB RAM [@MickLesk](https://github.com/MickLesk) ([#18472](https://github.com/community-scripts/ProxmoxVE/pull/18472))
|
||||
- Thingsboard: keep the package defaults in thingsboard.conf [@MickLesk](https://github.com/MickLesk) ([#18499](https://github.com/community-scripts/ProxmoxVE/pull/18499))
|
||||
- Nexterm: keep the extracted app out of the /tmp cleanup [@MickLesk](https://github.com/MickLesk) ([#18501](https://github.com/community-scripts/ProxmoxVE/pull/18501))
|
||||
- PLANKA: run the service with node instead of nodemon [@MickLesk](https://github.com/MickLesk) ([#18503](https://github.com/community-scripts/ProxmoxVE/pull/18503))
|
||||
- MeTube/Yubal: install Deno without its shell setup [@MickLesk](https://github.com/MickLesk) ([#18406](https://github.com/community-scripts/ProxmoxVE/pull/18406))
|
||||
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Technitium DNS: read the version from its GitHub releases [@MickLesk](https://github.com/MickLesk) ([#18425](https://github.com/community-scripts/ProxmoxVE/pull/18425))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
|
||||
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- ci: reject DevScripts URLs in scripts [@MickLesk](https://github.com/MickLesk) ([#18498](https://github.com/community-scripts/ProxmoxVE/pull/18498))
|
||||
|
||||
## 2026-11-05
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
|
||||
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
|
||||
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
|
||||
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
|
||||
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
|
||||
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
|
||||
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: keep the existing install until the new archive has extracted [@rcourtman](https://github.com/rcourtman) ([core#201](https://github.com/community-scripts/core/pull/201))
|
||||
- fix: keep the install when a clone or an Alpine extract fails [@rcourtman](https://github.com/rcourtman) ([core#210](https://github.com/community-scripts/core/pull/210))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Arcane: write both env names, stop shipping the unused .env [@MickLesk](https://github.com/MickLesk) ([#18466](https://github.com/community-scripts/ProxmoxVE/pull/18466))
|
||||
|
||||
## 2026-11-04
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Teleport: Poll readyz instead of fixed 10 second sleep. [@cthach](https://github.com/cthach) ([#18461](https://github.com/community-scripts/ProxmoxVE/pull/18461))
|
||||
- n8n: install past its unsatisfiable canvas dependencies [@MickLesk](https://github.com/MickLesk) ([#18439](https://github.com/community-scripts/ProxmoxVE/pull/18439))
|
||||
- Teable: add Redis cache [@MickLesk](https://github.com/MickLesk) ([#18450](https://github.com/community-scripts/ProxmoxVE/pull/18450))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- feat: say why download_gpg_key failed [@MickLesk](https://github.com/MickLesk) ([core#208](https://github.com/community-scripts/core/pull/208))
|
||||
- feat: read the MOTD's OS version at login [@MickLesk](https://github.com/MickLesk) ([core#207](https://github.com/community-scripts/core/pull/207))
|
||||
- feat: skip screen clear when there is no terminal | pin only the scripts own app with var_appversion [@MickLesk](https://github.com/MickLesk) ([core#203](https://github.com/community-scripts/core/pull/203))
|
||||
|
||||
## 2026-11-03
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fedora VM: let the engine install libguestfs-tools [@MickLesk](https://github.com/MickLesk) ([#18422](https://github.com/community-scripts/ProxmoxVE/pull/18422))
|
||||
- Cinephage: run the camoufox binary the package now ships [@MickLesk](https://github.com/MickLesk) ([#18426](https://github.com/community-scripts/ProxmoxVE/pull/18426))
|
||||
- AlmaLinux VM: report failures through the engine's error handler [@MickLesk](https://github.com/MickLesk) ([#18418](https://github.com/community-scripts/ProxmoxVE/pull/18418))
|
||||
- WireGuard: give the Debian container 1 GB for WGDashboard [@MickLesk](https://github.com/MickLesk) ([#18419](https://github.com/community-scripts/ProxmoxVE/pull/18419))
|
||||
- zabbix: use os specific apt packages [@MickLesk](https://github.com/MickLesk) ([#18417](https://github.com/community-scripts/ProxmoxVE/pull/18417))
|
||||
- Discourse: build libvips 8.18 for its image worker [@MickLesk](https://github.com/MickLesk) ([#18430](https://github.com/community-scripts/ProxmoxVE/pull/18430))
|
||||
- Wazuh: show the installer's own error when it fails [@MickLesk](https://github.com/MickLesk) ([#18428](https://github.com/community-scripts/ProxmoxVE/pull/18428))
|
||||
- Tracearr: move to Node.js 26 [@connorgallopo](https://github.com/connorgallopo) ([#18393](https://github.com/community-scripts/ProxmoxVE/pull/18393))
|
||||
|
||||
- #### ✨ New Features
|
||||
|
||||
- Wiki.js: install 3.x [@MickLesk](https://github.com/MickLesk) ([#18382](https://github.com/community-scripts/ProxmoxVE/pull/18382))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- OpenWrt VM: read the stable release from downloads.openwrt.org [@MickLesk](https://github.com/MickLesk) ([#18411](https://github.com/community-scripts/ProxmoxVE/pull/18411))
|
||||
- Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first [@MickLesk](https://github.com/MickLesk) ([#18397](https://github.com/community-scripts/ProxmoxVE/pull/18397))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- fix: put the reason for a VM failure into its trace [@MickLesk](https://github.com/MickLesk) ([core#200](https://github.com/community-scripts/core/pull/200))
|
||||
- fix: check the bridge before creating a container, and its state after start [@MickLesk](https://github.com/MickLesk) ([core#199](https://github.com/community-scripts/core/pull/199))
|
||||
- ref: offer to remove only the container this run created [@MickLesk](https://github.com/MickLesk) ([core#198](https://github.com/community-scripts/core/pull/198))
|
||||
- feat: report a cancelled tool dialog as an abort and name the failing command [@MickLesk](https://github.com/MickLesk) ([core#197](https://github.com/community-scripts/core/pull/197))
|
||||
- feat: check KVM and the host's threads before building a VM [@MickLesk](https://github.com/MickLesk) ([core#196](https://github.com/community-scripts/core/pull/196))
|
||||
- ref: give a host tool one log file for the whole run [@MickLesk](https://github.com/MickLesk) ([core#194](https://github.com/community-scripts/core/pull/194))
|
||||
- fix: keep a piped stdin when output moves to the log after a hangup [@MickLesk](https://github.com/MickLesk) ([core#193](https://github.com/community-scripts/core/pull/193))
|
||||
- fix: pve-LXC: say when the container went away during its setup [@MickLesk](https://github.com/MickLesk) ([core#192](https://github.com/community-scripts/core/pull/192))
|
||||
- fix: treat Esc in a VM wizard dialog as leaving the script [@MickLesk](https://github.com/MickLesk) ([core#190](https://github.com/community-scripts/core/pull/190))
|
||||
- ref: count a container stopped during a detached install as an abort [@MickLesk](https://github.com/MickLesk) ([core#187](https://github.com/community-scripts/core/pull/187))
|
||||
- feat: find the assets of a renamed GitHub repo without the API [@MickLesk](https://github.com/MickLesk) ([core#188](https://github.com/community-scripts/core/pull/188))
|
||||
- fix: restore npm when an interrupted update left it behind [@MickLesk](https://github.com/MickLesk) ([core#189](https://github.com/community-scripts/core/pull/189))
|
||||
- feat: add var_cmode to set the container console mode [@MickLesk](https://github.com/MickLesk) ([core#180](https://github.com/community-scripts/core/pull/180))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- all-templates: answering No or Cancel is not a failure [@MickLesk](https://github.com/MickLesk) ([#18423](https://github.com/community-scripts/ProxmoxVE/pull/18423))
|
||||
- homebrew: skip service accounts when picking the user, survive a failed apt update [@MickLesk](https://github.com/MickLesk) ([#18424](https://github.com/community-scripts/ProxmoxVE/pull/18424))
|
||||
- add-tailscale-lxc: start a stopped container and check its OS first [@MickLesk](https://github.com/MickLesk) ([#18421](https://github.com/community-scripts/ProxmoxVE/pull/18421))
|
||||
- netdata: survive a failed apt update, leave quietly on Cancel [@MickLesk](https://github.com/MickLesk) ([#18427](https://github.com/community-scripts/ProxmoxVE/pull/18427))
|
||||
- Privilege-Converter: offer only container-capable storages as the converter's restore target [@MickLesk](https://github.com/MickLesk) ([#18415](https://github.com/community-scripts/ProxmoxVE/pull/18415))
|
||||
- let post-pve-install go on when legacy sources are kept [@MickLesk](https://github.com/MickLesk) ([#18403](https://github.com/community-scripts/ProxmoxVE/pull/18403))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- update-apps: say why an backup or restore failed [@MickLesk](https://github.com/MickLesk) ([#18405](https://github.com/community-scripts/ProxmoxVE/pull/18405))
|
||||
- Keep host tools running when clear does not know the terminal [@MickLesk](https://github.com/MickLesk) ([#18412](https://github.com/community-scripts/ProxmoxVE/pull/18412))
|
||||
- Refuse to run the PBS 4 upgrade on a Proxmox VE host [@MickLesk](https://github.com/MickLesk) ([#18416](https://github.com/community-scripts/ProxmoxVE/pull/18416))
|
||||
- fstrim: keep trimming when pct fstrim fails on one container [@MickLesk](https://github.com/MickLesk) ([#18413](https://github.com/community-scripts/ProxmoxVE/pull/18413))
|
||||
- execute-lxc: do not abort on a container that is not Debian or Ubuntu [@MickLesk](https://github.com/MickLesk) ([#18414](https://github.com/community-scripts/ProxmoxVE/pull/18414))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- github: flag instead of closing on first PR template miss; add script-request eligibility check [@MickLesk](https://github.com/MickLesk) ([#18384](https://github.com/community-scripts/ProxmoxVE/pull/18384))
|
||||
|
||||
## 2026-11-02
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- FileFlows: replace the program folders on update [@MickLesk](https://github.com/MickLesk) ([#18380](https://github.com/community-scripts/ProxmoxVE/pull/18380))
|
||||
- anchor: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18373](https://github.com/community-scripts/ProxmoxVE/pull/18373))
|
||||
- guardian: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18375](https://github.com/community-scripts/ProxmoxVE/pull/18375))
|
||||
- nametag: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18376](https://github.com/community-scripts/ProxmoxVE/pull/18376))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- github: trim theater, add signal to the engine bug report; move AI block to end of PR template [@MickLesk](https://github.com/MickLesk) ([core#186](https://github.com/community-scripts/core/pull/186))
|
||||
- fix: do not wait forever for verbose output a daemon keeps open [@MickLesk](https://github.com/MickLesk) ([core#185](https://github.com/community-scripts/core/pull/185))
|
||||
|
||||
## 2026-11-01
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Amazon Linux ([#18369](https://github.com/community-scripts/ProxmoxVE/pull/18369))
|
||||
- CachyOS VM ([#18368](https://github.com/community-scripts/ProxmoxVE/pull/18368))
|
||||
- Razzia ([#18366](https://github.com/community-scripts/ProxmoxVE/pull/18366))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(zerobyte): build the bundled local agent [@nicotsx](https://github.com/nicotsx) ([#18363](https://github.com/community-scripts/ProxmoxVE/pull/18363))
|
||||
Generated
+8
-5
@@ -1,14 +1,17 @@
|
||||
# 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"
|
||||
target-branch: "main"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 5
|
||||
labels:
|
||||
- "dependencies"
|
||||
# One PR per week for all action bumps instead of one PR per action.
|
||||
groups:
|
||||
actions:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
Generated
+11
-11
@@ -1,4 +1,4 @@
|
||||
<!--🛑 New scripts must be submitted to [DevScripts](https://github.com/community-scripts/DevScripts) for testing.
|
||||
<!--🛑 New scripts must be submitted to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) 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,6 +17,16 @@ Fixes #
|
||||
**Tested on:** <!-- e.g. PVE 9.2 / Debian 13 / fresh install + update. Write "not tested" if you could not run it. -->
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI Assistance (**X** in brackets)
|
||||
|
||||
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
|
||||
> Select exactly one option.
|
||||
|
||||
- [ ] **No AI used** – Scripts were written without AI assistance.
|
||||
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/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)
|
||||
@@ -60,13 +70,3 @@ Guidance:
|
||||
-->
|
||||
|
||||
<!-- The advisory block is only active once it is OUTSIDE this comment. -->
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI Assistance (**X** in brackets)
|
||||
|
||||
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
|
||||
> Select exactly one option.
|
||||
|
||||
- [ ] **No AI used** – Scripts were written without AI assistance.
|
||||
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/DevScripts/blob/main/.github/agents/pve-script-creator.agent.md) as guidance, and the output has been reviewed and corrected to match those guidelines.
|
||||
Generated
-114
@@ -1,114 +0,0 @@
|
||||
name: ARM script test (manual)
|
||||
|
||||
# Builds one ct/ script on GitHub's free arm64 runner (ubuntu-24.04-arm),
|
||||
# inside Incus, with the same harness as pr-script-test.yml. Started by hand
|
||||
# from the Actions tab; it posts nothing anywhere: the result is in the job
|
||||
# summary and the artifact (with the screenshots).
|
||||
#
|
||||
# pr-script-test.yml runs the same ARM build for every /test, as information
|
||||
# only. This one is for trying a script or branch on ARM without a PR.
|
||||
#
|
||||
# Public repositories get the standard hosted runners, arm64 included, at no
|
||||
# cost. Each job is a fresh VM that is thrown away afterwards, so the script
|
||||
# under test never touches the lab.
|
||||
#
|
||||
# The engine refuses to guess on arm64: a script that leaves var_arm64 unset
|
||||
# stops with "refusing to guess". Here var_arm64=no means "not built", yes is
|
||||
# built as is, and unset is built with var_arm64=yes to see whether it would
|
||||
# work.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
script:
|
||||
description: "ct/ script, e.g. ct/bark-server.sh"
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
description: "Branch or full SHA of this repository"
|
||||
required: false
|
||||
default: main
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
arm:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
# The harness from the default branch, as in pr-script-test.yml.
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .github/workflows/scripts
|
||||
persist-credentials: false
|
||||
|
||||
- name: Decide var_arm64
|
||||
id: plan
|
||||
env:
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
REF: ${{ inputs.ref }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
[[ "$SCRIPT" =~ ^ct/[A-Za-z0-9._-]+\.sh$ ]] || { echo "::error::script must be ct/<name>.sh"; exit 1; }
|
||||
[[ "$REF" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "::error::bad ref"; exit 1; }
|
||||
sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)
|
||||
text=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${SCRIPT}?ref=${sha}" --jq .content | base64 -d)
|
||||
# A commented-out line ("#var_arm64=...") counts as unset.
|
||||
arm=$(grep -m1 -oE '^[[:space:]]*var_arm64="[$][{]var_arm64:-(yes|no)[}]"' <<<"$text" | grep -oE ':-(yes|no)' | cut -c3- || true)
|
||||
echo "sha=$sha" >>"$GITHUB_OUTPUT"
|
||||
echo "arm64=${arm:-unset}" >>"$GITHUB_OUTPUT"
|
||||
case "$arm" in
|
||||
no) echo "::notice::${SCRIPT} sets var_arm64=no: not built on ARM" ;;
|
||||
yes) echo "${SCRIPT} sets var_arm64=yes" ;;
|
||||
*) echo "::notice::${SCRIPT} leaves var_arm64 unset: building with var_arm64=yes to see whether it works" ;;
|
||||
esac
|
||||
echo "Building ${SCRIPT} at ${sha:0:7} on $(uname -m), $(nproc) cores, $(free -g | awk '/^Mem/ {print $2}') GB RAM, $(df -h / | awk 'NR==2 {print $4}') free"
|
||||
|
||||
- name: Set up Incus
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
|
||||
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
|
||||
|
||||
- name: Build and check ${{ inputs.script }}
|
||||
if: steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
PLATFORM: incus
|
||||
VAR_ARM64: "yes"
|
||||
PR_REPO: ${{ github.repository }}
|
||||
PR_SHA: ${{ steps.plan.outputs.sha }}
|
||||
SCRIPT: ${{ inputs.script }}
|
||||
UPSTREAM: ${{ github.repository }}
|
||||
CTID_BASE: "9000"
|
||||
GH_API_TOKEN: ${{ github.token }}
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
BUILD_TIMEOUT: 90m
|
||||
run: sudo --preserve-env=PLATFORM,VAR_ARM64,PR_REPO,PR_SHA,SCRIPT,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,BUILD_TIMEOUT,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT bash .github/workflows/scripts/pve-run-script.sh
|
||||
|
||||
- name: Summary
|
||||
if: always() && steps.plan.outputs.arm64 != 'no'
|
||||
env:
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
ARM64: ${{ steps.plan.outputs.arm64 }}
|
||||
run: |
|
||||
sudo chmod -R a+rX "$OUT_DIR" 2>/dev/null || true
|
||||
r="$OUT_DIR/result.json"
|
||||
[[ -f "$r" ]] || { echo "No result.json" >>"$GITHUB_STEP_SUMMARY"; exit 0; }
|
||||
{
|
||||
echo "### ARM (arm64, Incus): $(jq -r .script "$r")"
|
||||
echo
|
||||
echo "| Status | Install | Update | Duration | var_arm64 | Detail |"
|
||||
echo "|---|---|---|---|---|---|"
|
||||
jq -r --arg arm "$ARM64" '"| \(.status) | \(if .update_status != "" then "pass" else .status end) | \(if .update_status == "" then "-" else .update_status end) | \(.duration)s | \($arm) | \(if .status == "pass" then (.url // "" | if . == "" then "no URL" else . + " → " + $ARGS.named.c end) else .step end) |"' --arg c "$(jq -r .http_code "$r")" "$r"
|
||||
echo
|
||||
ls "$OUT_DIR"/screenshot*.png &>/dev/null && echo "Screenshots: in the \`result-arm\` artifact." || echo "No screenshot."
|
||||
} >>"$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: result-arm
|
||||
path: ${{ runner.temp }}/result
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
name: Auto Update .app-files
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "ct/**.sh"
|
||||
- "tools/**.sh"
|
||||
- "vm/**.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
update-app-files:
|
||||
if: github.repository == 'community-scripts/ProxmoxVE'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ vars.GHAPP_PR_ID }}
|
||||
private-key: ${{ secrets.GHAPP_PR_PRIVATE_KEY }}
|
||||
|
||||
- name: Generate a token for PR approval and merge
|
||||
id: generate-token-merge
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ vars.GHAPP_MERGEBOT_ID }}
|
||||
private-key: ${{ secrets.GHAPP_MERGEBOT_PRIVATE_KEY }}
|
||||
|
||||
# Step 1: Checkout repository
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Step 2: Disable file mode changes detection
|
||||
- name: Disable file mode changes
|
||||
run: git config core.fileMode false
|
||||
|
||||
# Step 3: Set up Git user for committing changes
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Step 4: Install figlet
|
||||
- name: Install figlet
|
||||
run: sudo apt-get install -y figlet
|
||||
|
||||
# Step 5: Run the updated generate-app-files.sh script
|
||||
- name: Run generate-app-files.sh
|
||||
run: |
|
||||
chmod +x .github/workflows/scripts/generate-app-headers.sh
|
||||
.github/workflows/scripts/generate-app-headers.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Step 6: Check if there are any changes
|
||||
- name: Check if there are any changes
|
||||
run: |
|
||||
echo "Checking for changes..."
|
||||
git add -A # Untracked Dateien aufnehmen
|
||||
git status
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes detected."
|
||||
echo "changed=false" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "Changes detected:"
|
||||
git diff --stat --cached
|
||||
echo "changed=true" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
# Step 7: Commit and create PR if changes exist
|
||||
- name: Commit and create PR if changes exist
|
||||
if: env.changed == 'true'
|
||||
run: |
|
||||
git commit -m "Update .app files"
|
||||
git checkout -b pr-update-app-files
|
||||
git push origin pr-update-app-files --force
|
||||
gh pr create --title "[core] update .app files" \
|
||||
--body "This PR is auto-generated by a GitHub Action to update the .app files." \
|
||||
--head pr-update-app-files \
|
||||
--base main \
|
||||
--label "automated pr"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Approve pull request
|
||||
if: env.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=$(gh pr list --head "pr-update-app-files" --json number --jq '.[].number')
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
gh pr review $PR_NUMBER --approve
|
||||
fi
|
||||
|
||||
- name: Approve pull request and merge
|
||||
if: env.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token-merge.outputs.token }}
|
||||
run: |
|
||||
git config --global user.name "github-actions-automege[bot]"
|
||||
git config --global user.email "github-actions-automege[bot]@users.noreply.github.com"
|
||||
PR_NUMBER=$(gh pr list --head "${BRANCH_NAME}" --json number --jq '.[].number')
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
gh pr review $PR_NUMBER --approve
|
||||
gh pr merge $PR_NUMBER --squash --admin
|
||||
fi
|
||||
|
||||
# Step 8: Output success message when no changes
|
||||
- name: No changes detected
|
||||
if: env.changed == 'false'
|
||||
run: echo "No changes to commit. Workflow completed successfully."
|
||||
Generated
+2
-2
@@ -126,7 +126,7 @@ jobs:
|
||||
try {
|
||||
const { data: relatedIssues } = await github.rest.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: "DevScripts",
|
||||
repo: "ProxmoxVED",
|
||||
state: "all",
|
||||
labels: ["Started Migration To ProxmoxVE"]
|
||||
});
|
||||
@@ -249,7 +249,7 @@ jobs:
|
||||
if (new Date(pr.merged_at) <= latestDateInChangelog) continue;
|
||||
|
||||
const rawLabels = pr.labels.map(label => label.name.toLowerCase());
|
||||
if (rawLabels.some(l => ["invalid", "wontdo", "wontfix", "duplicate", process.env.AUTOMATED_PR_LABEL].includes(l))) continue;
|
||||
if (rawLabels.some(l => ["invalid", "wontdo", "wontfix", "duplicate"].includes(l))) continue;
|
||||
|
||||
if (!coreCategory) {
|
||||
console.log(`No Core category configured; skipping core#${pr.number}`);
|
||||
|
||||
+1
-1
@@ -491,7 +491,7 @@ jobs:
|
||||
bash -c "\$(curl -fsSL "\$COMMUNITY_SCRIPTS_URL/ct/${slug}.sh")"
|
||||
\`\`\`
|
||||
|
||||
Both lines are needed — without \`COMMUNITY_SCRIPTS_URL\` the engine takes the \`install/\` script from \`main\`.
|
||||
Both lines are needed — each script pins \`_CS_DEFAULT_URL\` to \`main\`, and that pin is what fills \`COMMUNITY_SCRIPTS_URL\` when it is unset.
|
||||
|
||||
If the bump is wrong — upstream ships a newer Node than the app needs, or this script is deliberately held back — **close this PR**. It gets recorded on the drift report and this exact bump is not proposed again.
|
||||
PR_EOF
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
name: Check Script Repo URL
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "ct/**"
|
||||
- "install/**"
|
||||
- "vm/**"
|
||||
- "tools/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: github.repository == 'community-scripts/ProxmoxVE'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Reject DevScripts URLs
|
||||
run: |
|
||||
if grep -rnE 'community-scripts/(ProxmoxVED|DevScripts)/' ct install vm tools; then
|
||||
echo "::error::These lines point at DevScripts, so telemetry files the runs there. Use community-scripts/ProxmoxVE."
|
||||
exit 1
|
||||
fi
|
||||
+40
-44
@@ -1,4 +1,4 @@
|
||||
name: Flag PRs Missing Template
|
||||
name: Close PRs Missing Template
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -14,12 +14,10 @@ jobs:
|
||||
issues: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check PR against the template, comment and label instead of closing
|
||||
- name: Close PR if it does not follow the PR template
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const MARKER = "<!-- pr-template-check -->";
|
||||
const LABELS = ["missing pr template", "stale"];
|
||||
const pr = context.payload.pull_request;
|
||||
const prNumber = pr.number;
|
||||
const author = pr.user.login;
|
||||
@@ -46,11 +44,11 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
const prLabels = pr.labels.map((label) => label.name);
|
||||
const labels = pr.labels.map((label) => label.name);
|
||||
const skipLabels = ["automated pr", "keep-open"];
|
||||
|
||||
if (skipLabels.some((label) => prLabels.includes(label))) {
|
||||
core.info(`PR #${prNumber} has a skip label (${prLabels.join(", ")}) — skipping template validation.`);
|
||||
if (skipLabels.some((label) => labels.includes(label))) {
|
||||
core.info(`PR #${prNumber} has a skip label (${labels.join(", ")}) — skipping template validation.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,52 +114,50 @@ jobs:
|
||||
failures.push("At least one **Type of Change** checkbox must be checked.");
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: prNumber, per_page: 100
|
||||
});
|
||||
const mine = comments.find(c => c.body && c.body.includes(MARKER));
|
||||
|
||||
if (failures.length === 0) {
|
||||
core.info(`PR #${prNumber} follows the PR template.`);
|
||||
if (mine && !mine.body.startsWith(MARKER + "\n✅")) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: mine.id,
|
||||
body: MARKER + "\n✅ **Template check passed.** Thanks for fixing the description — the `stale` mark has been removed.",
|
||||
});
|
||||
}
|
||||
for (const name of LABELS) {
|
||||
if (prLabels.includes(name)) {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`PR #${prNumber} — template validation failed, flagging instead of closing.`);
|
||||
core.info(`Closing PR #${prNumber} — template validation failed.`);
|
||||
|
||||
const templateUrl =
|
||||
"https://github.com/community-scripts/ProxmoxVE/blob/main/.github/pull_request_template.md";
|
||||
const failureList = failures.map((item) => `- ${item}`).join("\n");
|
||||
|
||||
const text =
|
||||
MARKER + "\n" +
|
||||
`👋 Hi @${author},\n\n` +
|
||||
`This pull request does not follow the [PR template](${templateUrl}) yet:\n\n` +
|
||||
failureList + "\n\n" +
|
||||
`**Edit the PR description** to fix it — no new PR needed. This check runs again on every edit and clears itself once everything is in place.\n\n` +
|
||||
`Until then the PR is marked \`stale\` and will be closed in **7 days**.\n\n` +
|
||||
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.\n\n` +
|
||||
`Thank you for contributing! 🙏`;
|
||||
const comment = [
|
||||
`👋 Hi @${author},`,
|
||||
``,
|
||||
`This pull request was closed because it does not follow the [PR template](${templateUrl}).`,
|
||||
``,
|
||||
`Please fix the following and open a new PR (or reopen this one after updating the description):`,
|
||||
``,
|
||||
failureList,
|
||||
``,
|
||||
`> Use the template sections, fill in the description, check all prerequisite boxes, and select at least one type of change.`,
|
||||
``,
|
||||
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.`,
|
||||
``,
|
||||
`Thank you for contributing! 🙏`,
|
||||
].join("\n");
|
||||
|
||||
if (mine) {
|
||||
if (mine.body !== text) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body: text });
|
||||
}
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: text });
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: comment,
|
||||
});
|
||||
|
||||
const toAdd = LABELS.filter(name => !prLabels.includes(name));
|
||||
if (toAdd.length > 0) {
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: toAdd });
|
||||
}
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
labels: ["missing pr template"],
|
||||
});
|
||||
+2
-2
@@ -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 [**DevScripts**](https://github.com/community-scripts/DevScripts) for testing.`,
|
||||
`> 🛑 New scripts must be submitted to [**ProxmoxVED**](https://github.com/community-scripts/ProxmoxVED) for testing.`,
|
||||
`> PRs without prior testing will be closed.`,
|
||||
``,
|
||||
`Please open your PR at **https://github.com/community-scripts/DevScripts** instead.`,
|
||||
`Please open your PR at **https://github.com/community-scripts/ProxmoxVED** 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! 🙏`,
|
||||
|
||||
+11
-35
@@ -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/DevScripts
|
||||
DEV_REPO: community-scripts/ProxmoxVED
|
||||
|
||||
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 DevScripts issue
|
||||
- name: Resolve the matching ProxmoxVED 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|DevScripts)#[0-9]+' \
|
||||
| grep -oE 'community-scripts/ProxmoxVED#[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 DevScripts workflows use, so both
|
||||
# Same slug derivation the ProxmoxVED 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 DevScripts issues
|
||||
- name: Comment on and close matching ProxmoxVED 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&expand=type', {
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
headers: { 'Authorization': token }
|
||||
});
|
||||
const list = JSON.parse(listRes.body);
|
||||
@@ -239,21 +239,14 @@ jobs:
|
||||
console.log('Slug not in DB, skipping: ' + slug);
|
||||
continue;
|
||||
}
|
||||
const patch = {
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
};
|
||||
// Containers in ProxmoxVE are synced to Incus from here on.
|
||||
const recordType = record.expand && record.expand.type && record.expand.type.type;
|
||||
const platforms = Array.isArray(record.platforms) ? record.platforms : [];
|
||||
if (recordType === 'lxc' && !platforms.includes('incus')) patch.platforms = ['pve', 'incus'];
|
||||
else if (!platforms.length) patch.platforms = ['pve'];
|
||||
if (!Array.isArray(record.architectures) || !record.architectures.length) patch.architectures = ['amd64'];
|
||||
const patchRes = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(patch)
|
||||
body: JSON.stringify({
|
||||
name: record.name || record.slug,
|
||||
last_update_commit: prUrl,
|
||||
is_dev: false
|
||||
})
|
||||
});
|
||||
if (!patchRes.ok) {
|
||||
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
|
||||
@@ -265,20 +258,3 @@ 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"
|
||||
+3
-2
@@ -267,14 +267,15 @@ jobs:
|
||||
const stubPath = path.join('ct', slug + '.sh');
|
||||
const content = [
|
||||
"#!/usr/bin/env bash",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/core/main/core/core.func)",
|
||||
"source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.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, '\\"')}"`,
|
||||
|
||||
Generated
+626
@@ -0,0 +1,626 @@
|
||||
name: PocketBase AI Bot
|
||||
|
||||
# Natural-language companion to pocketbase-bot.yml.
|
||||
# Mention the bot in plain English, e.g.:
|
||||
# @pocketbase-bot change RAM to 4096 on zigbee2mqtt
|
||||
# @pocketbase-bot disable script Nextcloud because upstream is broken
|
||||
# The bot parses the request with GitHub Models, replies with the exact change(s)
|
||||
# it understood, and only applies them after you reply "@pocketbase-bot confirm".
|
||||
# The slash-command bot (/pocketbase ...) is unaffected; triggers do not overlap.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
models: read # lets the built-in GITHUB_TOKEN call GitHub Models inference
|
||||
contents: write # built-in token opens the CT-defaults sync PR (like the slash bot)
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
ai-bot:
|
||||
runs-on: self-hosted
|
||||
# Broad gate; the script does precise keyword + self-author checks.
|
||||
if: contains(github.event.comment.body, '@pocketbase-bot')
|
||||
|
||||
steps:
|
||||
- name: Mint GitHub App token (bot identity)
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
app-id: ${{ vars.GHAPP_PBBOT_ID }}
|
||||
private-key: ${{ secrets.GHAPP_PBBOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Run PocketBase AI bot
|
||||
env:
|
||||
# GitHub REST as the bot identity
|
||||
GH_APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
PB_BOT_APP_ID: ${{ vars.GHAPP_PBBOT_ID }}
|
||||
# GitHub Models inference uses the built-in token (needs models: read)
|
||||
MODELS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Built-in token for git/PR ops (CT-defaults sync), mirroring the slash bot
|
||||
GH_DEFAULT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AI_MODEL: openai/gpt-4o
|
||||
# PocketBase
|
||||
POCKETBASE_URL: ${{ vars.POCKETBASE_URL }}
|
||||
POCKETBASE_COLLECTION: ${{ vars.POCKETBASE_COLLECTION }}
|
||||
POCKETBASE_ADMIN_EMAIL: ${{ secrets.POCKETBASE_ADMIN_EMAIL }}
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
FRONTEND_URL: ${{ vars.FRONTEND_URL }}
|
||||
REVALIDATE_SECRET: ${{ secrets.FRONTEND_INGEST_SECRET }}
|
||||
# Event context
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
COMMENT_AUTHOR_TYPE: ${{ github.event.comment.user.type }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
ACTOR: ${{ github.event.comment.user.login }}
|
||||
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
|
||||
// ── HTTP helper with redirect following ────────────────────────────
|
||||
function request(fullUrl, opts, redirectCount) {
|
||||
redirectCount = redirectCount || 0;
|
||||
return new Promise(function (resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
const body = opts.body;
|
||||
const options = {
|
||||
hostname: u.hostname,
|
||||
port: u.port || (isHttps ? 443 : 80),
|
||||
path: u.path,
|
||||
method: opts.method || 'GET',
|
||||
headers: opts.headers || {}
|
||||
};
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function (res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
|
||||
const redirectUrl = url.resolve(fullUrl, res.headers.location);
|
||||
res.resume();
|
||||
resolve(request(redirectUrl, opts, redirectCount + 1));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', function (chunk) { data += chunk; });
|
||||
res.on('end', function () {
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, statusCode: res.statusCode, body: data });
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ── GitHub REST (as the bot app) ───────────────────────────────────
|
||||
const owner = process.env.REPO_OWNER;
|
||||
const repo = process.env.REPO_NAME;
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
|
||||
const commentId = parseInt(process.env.COMMENT_ID, 10);
|
||||
const actor = process.env.ACTOR;
|
||||
|
||||
function ghRequest(path, method, body) {
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GH_APP_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PocketBase-AI-Bot'
|
||||
};
|
||||
const bodyStr = body ? JSON.stringify(body) : undefined;
|
||||
if (bodyStr) headers['Content-Type'] = 'application/json';
|
||||
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
|
||||
}
|
||||
|
||||
// Same as ghRequest but authenticated with the built-in GITHUB_TOKEN.
|
||||
// Used for the CT-defaults sync branch/PR (the App token lacks contents:write).
|
||||
function ghDefault(path, method, body) {
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GH_DEFAULT_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'PocketBase-AI-Bot'
|
||||
};
|
||||
const bodyStr = body ? JSON.stringify(body) : undefined;
|
||||
if (bodyStr) headers['Content-Type'] = 'application/json';
|
||||
return request('https://api.github.com' + path, { method: method || 'GET', headers, body: bodyStr });
|
||||
}
|
||||
|
||||
async function addReaction(content) {
|
||||
try {
|
||||
await ghRequest('/repos/' + owner + '/' + repo + '/issues/comments/' + commentId + '/reactions', 'POST', { content });
|
||||
} catch (e) { console.warn('Could not add reaction:', e.message); }
|
||||
}
|
||||
async function postComment(text) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments', 'POST', { body: text });
|
||||
if (!res.ok) console.warn('Could not post comment:', res.body);
|
||||
return res.ok ? JSON.parse(res.body) : null;
|
||||
}
|
||||
async function updateComment(id, text) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/comments/' + id, 'PATCH', { body: text });
|
||||
if (!res.ok) console.warn('Could not update comment:', res.body);
|
||||
}
|
||||
async function listIssueComments() {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
while (page <= 10) {
|
||||
const res = await ghRequest('/repos/' + owner + '/' + repo + '/issues/' + issueNumber + '/comments?per_page=100&page=' + page);
|
||||
if (!res.ok) break;
|
||||
const batch = JSON.parse(res.body);
|
||||
all.push.apply(all, batch);
|
||||
if (batch.length < 100) break;
|
||||
page++;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
// ── 1. Self-trigger guard (App-token comments DO re-fire this event) ─
|
||||
if (process.env.COMMENT_AUTHOR_TYPE === 'Bot') {
|
||||
console.log('Comment authored by a bot — skipping to avoid loops.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. Permission gate (mirrors the slash bot) ─────────────────────
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase AI Bot**: @' + actor + ' is not authorized to use this command.\n' +
|
||||
'Only org members (Contributors team) can use `@pocketbase-bot`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 3. Extract the instruction after the @pocketbase-bot handle ────
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
const handleMatch = commentBody.match(/@pocketbase-bot(\[bot\])?/i);
|
||||
if (!handleMatch) {
|
||||
console.log('No @pocketbase-bot handle found — ignoring.');
|
||||
return;
|
||||
}
|
||||
const instruction = commentBody.slice(handleMatch.index + handleMatch[0].length).trim();
|
||||
if (!instruction) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'ℹ️ **PocketBase AI Bot**: Tell me what to do, e.g.\n' +
|
||||
'`@pocketbase-bot change RAM to 4096 on zigbee2mqtt` or `@pocketbase-bot disable script Nextcloud`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── PocketBase auth + low-level helpers ────────────────────────────
|
||||
const pbRaw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||
const apiBase = /\/api$/i.test(pbRaw) ? pbRaw : pbRaw + '/api';
|
||||
const coll = process.env.POCKETBASE_COLLECTION;
|
||||
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||
|
||||
async function pbAuth() {
|
||||
const res = await request(apiBase + '/collections/users/auth-with-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identity: process.env.POCKETBASE_ADMIN_EMAIL, password: process.env.POCKETBASE_ADMIN_PASSWORD })
|
||||
});
|
||||
if (!res.ok) throw new Error('PocketBase auth failed: ' + res.body);
|
||||
return JSON.parse(res.body).token;
|
||||
}
|
||||
async function pbFindRecord(token, slug) {
|
||||
const filter = "(slug='" + String(slug).replace(/'/g, "''") + "')";
|
||||
const res = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', { headers: { 'Authorization': token } });
|
||||
const list = JSON.parse(res.body);
|
||||
return list.items && list.items[0];
|
||||
}
|
||||
async function pbPatch(token, id, payload) {
|
||||
return request(recordsUrl + '/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
function readJsonBlob(val) {
|
||||
if (Array.isArray(val)) return val;
|
||||
try { return JSON.parse(val || '[]'); } catch (e) { return []; }
|
||||
}
|
||||
async function revalidate(s) {
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
const secret = process.env.REVALIDATE_SECRET;
|
||||
if (!frontendUrl || !secret) return;
|
||||
try {
|
||||
await request(frontendUrl.replace(/\/$/, '') + '/api/revalidate', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + secret, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tags: ['scripts', 'script-' + s] })
|
||||
});
|
||||
} catch (e) { console.warn('Revalidation skipped:', e.message); }
|
||||
}
|
||||
|
||||
// ── CT-defaults sync PR (copied from slash bot) ────────────────────
|
||||
function encodeContentPath(filePath) { return filePath.split('/').map(encodeURIComponent).join('/'); }
|
||||
function decodeGitHubContent(content) { return Buffer.from((content || '').replace(/\n/g, ''), 'base64').toString('utf8'); }
|
||||
function sanitizeBranchPart(value) {
|
||||
return (value || '').toLowerCase().replace(/[^a-z0-9._/-]+/g, '-').replace(/\/+/g, '/').replace(/^-+|-+$/g, '');
|
||||
}
|
||||
function applyCtDefaultChanges(scriptText, varChanges) {
|
||||
let nextText = scriptText;
|
||||
const updatedVars = [], unchangedVars = [];
|
||||
for (const [varName, rawValue] of Object.entries(varChanges)) {
|
||||
const newValue = String(rawValue);
|
||||
const pattern = new RegExp('(^\\s*' + varName + '="\\$\\{' + varName + ':-)([^"}]*)(\\}"\\s*$)', 'm');
|
||||
const match = nextText.match(pattern);
|
||||
if (!match) continue;
|
||||
if (match[2] === newValue) { unchangedVars.push(varName); continue; }
|
||||
nextText = nextText.replace(pattern, '$1' + newValue + '$3');
|
||||
updatedVars.push(varName);
|
||||
}
|
||||
return { nextText, updatedVars, unchangedVars };
|
||||
}
|
||||
async function ensureBranch(defaultBranch, branchName) {
|
||||
const ghRequest = ghDefault; // git ops run as the built-in token
|
||||
const branchRefRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/ref/heads/' + encodeURIComponent(branchName));
|
||||
if (branchRefRes.ok) return;
|
||||
const defaultRefRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/ref/heads/' + encodeURIComponent(defaultBranch));
|
||||
if (!defaultRefRes.ok) throw new Error('Could not read default branch ref: ' + defaultRefRes.body);
|
||||
const defaultRef = JSON.parse(defaultRefRes.body);
|
||||
const createBranchRes = await ghRequest('/repos/' + owner + '/' + repo + '/git/refs', 'POST', { ref: 'refs/heads/' + branchName, sha: defaultRef.object.sha });
|
||||
if (!createBranchRes.ok) throw new Error('Could not create branch: ' + createBranchRes.body);
|
||||
}
|
||||
async function upsertCtDefaultsPr(slugValue, varChanges) {
|
||||
const ghRequest = ghDefault; // contents/PR ops run as the built-in token
|
||||
const wantedEntries = Object.entries(varChanges || {}).filter(function ([, v]) { return v !== undefined && v !== null && String(v) !== ''; });
|
||||
if (wantedEntries.length === 0) return { status: 'skipped', reason: 'No mapped CT defaults changed.' };
|
||||
const repoRes = await ghRequest('/repos/' + owner + '/' + repo);
|
||||
if (!repoRes.ok) throw new Error('Could not read repository metadata: ' + repoRes.body);
|
||||
const defaultBranch = JSON.parse(repoRes.body).default_branch;
|
||||
const ctPath = 'ct/' + slugValue + '.sh';
|
||||
const encodedCtPath = encodeContentPath(ctPath);
|
||||
const defaultFileRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath + '?ref=' + encodeURIComponent(defaultBranch));
|
||||
if (defaultFileRes.statusCode === 404) return { status: 'skipped', reason: 'No matching CT file found at `' + ctPath + '`.' };
|
||||
if (!defaultFileRes.ok) throw new Error('Could not read CT file from default branch: ' + defaultFileRes.body);
|
||||
const branchName = 'pocketbase-sync/' + sanitizeBranchPart(slugValue || 'unknown');
|
||||
await ensureBranch(defaultBranch, branchName);
|
||||
const branchFileRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath + '?ref=' + encodeURIComponent(branchName));
|
||||
if (!branchFileRes.ok) throw new Error('Could not read CT file from sync branch: ' + branchFileRes.body);
|
||||
const branchFile = JSON.parse(branchFileRes.body);
|
||||
const currentBranchText = decodeGitHubContent(branchFile.content);
|
||||
const updateResult = applyCtDefaultChanges(currentBranchText, Object.fromEntries(wantedEntries));
|
||||
if (updateResult.updatedVars.length === 0) return { status: 'skipped', reason: 'CT defaults already up to date.' };
|
||||
const putRes = await ghRequest('/repos/' + owner + '/' + repo + '/contents/' + encodedCtPath, 'PUT', {
|
||||
message: 'chore(ct): sync ' + slugValue + ' defaults from PocketBase',
|
||||
content: Buffer.from(updateResult.nextText, 'utf8').toString('base64'),
|
||||
sha: branchFile.sha,
|
||||
branch: branchName
|
||||
});
|
||||
if (!putRes.ok) throw new Error('Could not update CT file: ' + putRes.body);
|
||||
const openPrRes = await ghRequest('/repos/' + owner + '/' + repo + '/pulls?state=open&head=' + encodeURIComponent(owner + ':' + branchName) + '&base=' + encodeURIComponent(defaultBranch));
|
||||
if (!openPrRes.ok) throw new Error('Could not query existing PRs: ' + openPrRes.body);
|
||||
const openPrs = JSON.parse(openPrRes.body);
|
||||
if (openPrs.length > 0) return { status: 'updated', prUrl: openPrs[0].html_url, updatedVars: updateResult.updatedVars };
|
||||
const createPrRes = await ghRequest('/repos/' + owner + '/' + repo + '/pulls', 'POST', {
|
||||
title: 'chore(ct): sync ' + slugValue + ' defaults with PocketBase',
|
||||
body: '## Summary\n- Sync default CT variables for `' + slugValue + '` after an `@pocketbase-bot` update.\n- Updated vars: `' + updateResult.updatedVars.join('`, `') + '`.\n\n## Source\n- Triggered by @' + actor + ' via PocketBase AI bot.\n',
|
||||
head: branchName,
|
||||
base: defaultBranch
|
||||
});
|
||||
if (!createPrRes.ok) throw new Error('Could not create PR: ' + createPrRes.body);
|
||||
return { status: 'created', prUrl: JSON.parse(createPrRes.body).html_url, updatedVars: updateResult.updatedVars };
|
||||
}
|
||||
|
||||
// ── Allow-lists (mirror the slash bot) ─────────────────────────────
|
||||
const ALLOWED_FIELDS = {
|
||||
name: 'string', description: 'string', logo: 'string', documentation: 'string',
|
||||
website: 'string', project_url: 'string', repository: 'string', config_path: 'string',
|
||||
tags: 'string', port: 'number', default_user: 'nullable_string', default_passwd: 'nullable_string',
|
||||
unprivileged: 'number', updateable: 'boolean', privileged: 'boolean',
|
||||
architectures: 'select_list', platforms: 'select_list',
|
||||
is_dev: 'boolean', is_disabled: 'boolean', disable_message: 'string',
|
||||
is_deleted: 'boolean', deleted_message: 'string'
|
||||
};
|
||||
const SELECT_VALUES = {
|
||||
architectures: ['amd64', 'arm64'],
|
||||
platforms: ['pve', 'incus'],
|
||||
};
|
||||
const FIELD_TO_CT_VAR = { tags: 'var_tags', unprivileged: 'var_unprivileged' };
|
||||
const RESOURCE_KEYS = { cpu: 'number', ram: 'number', hdd: 'number', os: 'string', version: 'string' };
|
||||
const METHOD_KEYS = { config_path: 'string', script: 'string' };
|
||||
const ALL_METHOD_KEYS = Object.assign({}, RESOURCE_KEYS, METHOD_KEYS);
|
||||
const RESOURCE_TO_CT_VAR = { cpu: 'var_cpu', ram: 'var_ram', hdd: 'var_disk', os: 'var_os', version: 'var_version' };
|
||||
|
||||
function castFieldValue(key, rawVal) {
|
||||
const type = ALLOWED_FIELDS[key];
|
||||
if (!type) return { error: 'Unknown field `' + key + '`' };
|
||||
if (type === 'boolean') {
|
||||
if (rawVal === true || rawVal === 'true') return { value: true };
|
||||
if (rawVal === false || rawVal === 'false') return { value: false };
|
||||
return { error: '`' + key + '` must be true/false' };
|
||||
}
|
||||
if (type === 'number') {
|
||||
const n = parseInt(rawVal, 10);
|
||||
if (isNaN(n)) return { error: '`' + key + '` must be a number' };
|
||||
return { value: n };
|
||||
}
|
||||
if (type === 'select_list') {
|
||||
// architectures/platforms are multi-selects over a closed set,
|
||||
// so an unknown value is rejected rather than stored. The model
|
||||
// may hand back an array or a comma-separated string.
|
||||
const allowed = SELECT_VALUES[key] || [];
|
||||
const raw = Array.isArray(rawVal) ? rawVal : String(rawVal == null ? '' : rawVal).split(',');
|
||||
const values = raw.map(function (v) { return String(v).trim(); }).filter(Boolean);
|
||||
const bad = values.filter(function (v) { return allowed.indexOf(v) === -1; });
|
||||
if (bad.length > 0) return { error: '`' + key + '` accepts ' + allowed.join(', ') + ' — got: ' + bad.join(', ') };
|
||||
return { value: values };
|
||||
}
|
||||
if (type === 'nullable_string') return { value: rawVal === '' || rawVal == null ? null : String(rawVal) };
|
||||
return { value: String(rawVal) };
|
||||
}
|
||||
|
||||
// ── Operation validation (used at propose AND confirm time) ────────
|
||||
// Never trust raw operations: enforce the field/op allow-lists and
|
||||
// re-cast values. Returns only well-formed, allowed operations.
|
||||
function sanitizeOperations(ops) {
|
||||
const validOps = [], problems = [];
|
||||
for (const op of (Array.isArray(ops) ? ops : [])) {
|
||||
if (op && op.kind === 'field') {
|
||||
const cast = castFieldValue(op.field, op.value);
|
||||
if (cast.error) { problems.push(cast.error); continue; }
|
||||
validOps.push({ kind: 'field', field: op.field, value: cast.value });
|
||||
} else if (op && op.kind === 'note' && ['add', 'edit', 'remove'].includes(op.action)) {
|
||||
validOps.push({ kind: 'note', action: op.action, type: String(op.type || ''), text: op.text, newText: op.newText });
|
||||
} else if (op && op.kind === 'method' && ['add', 'edit', 'remove'].includes(op.action)) {
|
||||
const changes = {};
|
||||
for (const [k, v] of Object.entries(op.changes || {})) { if (ALL_METHOD_KEYS[k]) changes[k] = v; }
|
||||
validOps.push({ kind: 'method', action: op.action, type: String(op.type || 'default'), changes });
|
||||
} else {
|
||||
problems.push('Unsupported operation: `' + JSON.stringify(op) + '`');
|
||||
}
|
||||
}
|
||||
return { validOps, problems };
|
||||
}
|
||||
|
||||
// ── Executor: apply a validated {slug, operations} set ─────────────
|
||||
async function applyOperations(action) {
|
||||
const token = await pbAuth();
|
||||
const record = await pbFindRecord(token, action.slug);
|
||||
if (!record) return { ok: false, summary: '❌ No PocketBase record for slug `' + action.slug + '`.' };
|
||||
|
||||
const fieldPayload = {};
|
||||
let notesArr = readJsonBlob(record.notes);
|
||||
let methodsArr = readJsonBlob(record.install_methods);
|
||||
let notesChanged = false, methodsChanged = false;
|
||||
const ctChanges = {};
|
||||
const lines = [];
|
||||
|
||||
for (const op of action.operations) {
|
||||
if (op.kind === 'field') {
|
||||
const cast = castFieldValue(op.field, op.value);
|
||||
if (cast.error) { lines.push('- ⚠️ skipped field: ' + cast.error); continue; }
|
||||
fieldPayload[op.field] = cast.value;
|
||||
if (FIELD_TO_CT_VAR[op.field]) ctChanges[FIELD_TO_CT_VAR[op.field]] = cast.value;
|
||||
lines.push('- `' + op.field + '` → `' + JSON.stringify(cast.value) + '`');
|
||||
} else if (op.kind === 'note') {
|
||||
const type = String(op.type || '').toLowerCase();
|
||||
if (op.action === 'add') {
|
||||
notesArr.push({ type, text: String(op.text || '') });
|
||||
notesChanged = true; lines.push('- note add `' + type + '`: ' + op.text);
|
||||
} else if (op.action === 'remove') {
|
||||
const before = notesArr.length;
|
||||
notesArr = notesArr.filter(function (n) { return !(String(n.type).toLowerCase() === type && n.text === op.text); });
|
||||
if (notesArr.length !== before) { notesChanged = true; lines.push('- note remove `' + type + '`: ' + op.text); }
|
||||
else lines.push('- ⚠️ note remove: no `' + type + '` note matched');
|
||||
} else if (op.action === 'edit') {
|
||||
const idx = notesArr.findIndex(function (n) { return String(n.type).toLowerCase() === type && n.text === op.text; });
|
||||
if (idx !== -1) { notesArr[idx].text = String(op.newText || ''); notesChanged = true; lines.push('- note edit `' + type + '`'); }
|
||||
else lines.push('- ⚠️ note edit: no `' + type + '` note matched');
|
||||
}
|
||||
} else if (op.kind === 'method') {
|
||||
const type = String(op.type || '').toLowerCase();
|
||||
const changes = op.changes || {};
|
||||
if (op.action === 'remove') {
|
||||
const before = methodsArr.length;
|
||||
methodsArr = methodsArr.filter(function (im) { return String(im.type || '').toLowerCase() !== type; });
|
||||
if (methodsArr.length !== before) { methodsChanged = true; lines.push('- method remove `' + type + '`'); }
|
||||
else lines.push('- ⚠️ method remove: `' + type + '` not found');
|
||||
} else {
|
||||
let method = methodsArr.find(function (im) { return String(im.type || '').toLowerCase() === type; });
|
||||
if (!method && op.action === 'add') { method = { type, resources: { cpu: 1, ram: 512, hdd: 4, os: 'debian', version: '13' } }; methodsArr.push(method); }
|
||||
if (!method) { lines.push('- ⚠️ method edit: `' + type + '` not found'); continue; }
|
||||
if (!method.resources) method.resources = {};
|
||||
for (const [k, v] of Object.entries(changes)) {
|
||||
if (RESOURCE_KEYS[k]) {
|
||||
method.resources[k] = RESOURCE_KEYS[k] === 'number' ? parseInt(v, 10) : String(v);
|
||||
if (RESOURCE_TO_CT_VAR[k]) ctChanges[RESOURCE_TO_CT_VAR[k]] = method.resources[k];
|
||||
} else if (METHOD_KEYS[k]) {
|
||||
method[k] = v === '' ? null : String(v);
|
||||
}
|
||||
}
|
||||
methodsChanged = true;
|
||||
lines.push('- method `' + (op.action === 'add' ? 'add' : 'edit') + '` `' + type + '`: ' + JSON.stringify(changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(fieldPayload).length) {
|
||||
const r = await pbPatch(token, record.id, fieldPayload);
|
||||
if (!r.ok) return { ok: false, summary: '❌ Field update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
if (notesChanged) {
|
||||
const r = await pbPatch(token, record.id, { notes: notesArr });
|
||||
if (!r.ok) return { ok: false, summary: '❌ Notes update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
if (methodsChanged) {
|
||||
const r = await pbPatch(token, record.id, { install_methods: methodsArr });
|
||||
if (!r.ok) return { ok: false, summary: '❌ Install-method update failed:\n```\n' + r.body + '\n```' };
|
||||
}
|
||||
await revalidate(action.slug);
|
||||
|
||||
let ctNote = '';
|
||||
if (Object.keys(ctChanges).length) {
|
||||
try {
|
||||
const sync = await upsertCtDefaultsPr(action.slug, ctChanges);
|
||||
if (sync.status === 'created') ctNote = '\n\n**CT sync PR:** ' + sync.prUrl;
|
||||
else if (sync.status === 'updated') ctNote = '\n\n**CT sync PR updated:** ' + sync.prUrl;
|
||||
else if (sync.status === 'skipped') ctNote = '\n\n**CT sync skipped:** ' + sync.reason;
|
||||
} catch (e) { ctNote = '\n\n**CT sync failed:** ' + e.message; }
|
||||
}
|
||||
return { ok: true, summary: lines.join('\n') + ctNote };
|
||||
}
|
||||
|
||||
// ── 4. Confirm branch ──────────────────────────────────────────────
|
||||
const PENDING_RE = /<!--\s*pocketbase-pending:\s*([A-Za-z0-9+/=]+)\s*-->/;
|
||||
const isConfirm = /^(confirm|yes|apply|do it|y)\b/i.test(instruction);
|
||||
|
||||
if (isConfirm) {
|
||||
const comments = await listIssueComments();
|
||||
const appId = String(process.env.PB_BOT_APP_ID || '');
|
||||
let pending = null, pendingComment = null;
|
||||
for (let i = comments.length - 1; i >= 0; i--) {
|
||||
const c = comments[i];
|
||||
// Only trust a marker in a comment THIS bot app authored — otherwise a
|
||||
// user could hand-craft a forged pocketbase-pending marker and confirm it.
|
||||
const byBotApp = c.user && c.user.type === 'Bot' &&
|
||||
c.performed_via_github_app && String(c.performed_via_github_app.id) === appId;
|
||||
if (!byBotApp) continue;
|
||||
const m = c.body && c.body.match(PENDING_RE);
|
||||
if (m) { pending = m[1]; pendingComment = c; break; }
|
||||
}
|
||||
if (!pending) {
|
||||
await addReaction('confused');
|
||||
await postComment('🤔 **PocketBase AI Bot**: I have no pending change to confirm in this thread.');
|
||||
return;
|
||||
}
|
||||
let action;
|
||||
try { action = JSON.parse(Buffer.from(pending, 'base64').toString('utf8')); }
|
||||
catch (e) { await postComment('❌ **PocketBase AI Bot**: Could not decode the pending change.'); return; }
|
||||
|
||||
// Re-validate the decoded operations before applying (defense-in-depth).
|
||||
const recheck = sanitizeOperations(action.operations);
|
||||
if (!action.slug || recheck.validOps.length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: The pending change is no longer valid. Please restate the request.');
|
||||
return;
|
||||
}
|
||||
action.operations = recheck.validOps;
|
||||
|
||||
let result;
|
||||
try { result = await applyOperations(action); }
|
||||
catch (e) { await addReaction('-1'); await postComment('❌ **PocketBase AI Bot**: ' + e.message); return; }
|
||||
|
||||
if (!result.ok) { await addReaction('-1'); await postComment(result.summary); return; }
|
||||
await updateComment(pendingComment.id, pendingComment.body.replace(PENDING_RE, '<!-- pocketbase-applied -->'));
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase AI Bot**: Applied to **`' + action.slug + '`**\n\n' + result.summary +
|
||||
'\n\n*Confirmed by @' + actor + '*'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 5. New request: acknowledge, fetch script list, call the model ─
|
||||
await addReaction('eyes');
|
||||
|
||||
const token0 = await pbAuth();
|
||||
const scripts = [];
|
||||
let page = 1;
|
||||
while (page <= 5) {
|
||||
const res = await request(recordsUrl + '?fields=slug,name&perPage=500&page=' + page, { headers: { 'Authorization': token0 } });
|
||||
if (!res.ok) break;
|
||||
const data = JSON.parse(res.body);
|
||||
(data.items || []).forEach(function (it) { if (it.slug) scripts.push({ slug: it.slug, name: it.name || it.slug }); });
|
||||
if (!data.items || data.items.length < 500) break;
|
||||
page++;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
'You translate a maintainer\'s natural-language request into a STRICT JSON change-set for a ' +
|
||||
'script catalog (PocketBase). Respond with a SINGLE JSON object and nothing else.\n\n' +
|
||||
'Schema:\n' +
|
||||
'{\n' +
|
||||
' "slug": string|null, // MUST be one of the known slugs below, chosen from the request\n' +
|
||||
' "operations": [ ... ], // [] if you cannot determine concrete changes\n' +
|
||||
' "human_summary": string, // short plain-English description\n' +
|
||||
' "clarification": string|null // a question to ask if ambiguous/unsupported; else null\n' +
|
||||
'}\n\n' +
|
||||
'Operation kinds:\n' +
|
||||
'- {"kind":"field","field":<one of: ' + Object.keys(ALLOWED_FIELDS).join(', ') + '>,"value":<bool|number|string>}\n' +
|
||||
' (booleans true/false; "is_disabled"/"is_deleted"/"is_dev" are booleans; "port"/"unprivileged" are numbers; rest strings.)\n' +
|
||||
'- {"kind":"note","action":"add"|"edit"|"remove","type":string,"text":string,"newText":string?}\n' +
|
||||
'- {"kind":"method","action":"add"|"edit"|"remove","type":string,"changes":{cpu?:number,ram?:number,hdd?:number,os?:string,version?:string,config_path?:string,script?:string}}\n' +
|
||||
' (RAM/HDD are in MB/GB; method "type" defaults to "default" if the user does not name one.)\n\n' +
|
||||
'Rules:\n' +
|
||||
'- Only use fields/operations listed above. If the request needs something else, set clarification and operations=[].\n' +
|
||||
'- "disable"/"enable" map to is_disabled true/false. If disabling and the user gave a reason, also set disable_message.\n' +
|
||||
'- Resolve the target script to a slug from the list. If you cannot confidently match exactly one, set slug=null and ask via clarification.\n\n' +
|
||||
'Known scripts (slug — name):\n' +
|
||||
scripts.map(function (s) { return s.slug + ' — ' + s.name; }).join('\n');
|
||||
|
||||
const modelRes = await request('https://models.github.ai/inference/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + process.env.MODELS_TOKEN, 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: process.env.AI_MODEL || 'openai/gpt-4o',
|
||||
temperature: 0.1,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [{ role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: instruction }]
|
||||
})
|
||||
});
|
||||
if (!modelRes.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: Model request failed (' + modelRes.statusCode + ').\n```\n' + modelRes.body.slice(0, 500) + '\n```');
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
const content = JSON.parse(modelRes.body).choices[0].message.content;
|
||||
const cleaned = content.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
||||
parsed = JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase AI Bot**: Could not parse the model response. Please rephrase.');
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 6. Validate ────────────────────────────────────────────────────
|
||||
const knownSlugs = new Set(scripts.map(function (s) { return s.slug; }));
|
||||
const problems = [];
|
||||
if (parsed.clarification) problems.push(parsed.clarification);
|
||||
if (!parsed.slug || !knownSlugs.has(parsed.slug)) problems.push('I could not match the request to a known script.');
|
||||
const sanitized = sanitizeOperations(parsed.operations);
|
||||
const validOps = sanitized.validOps;
|
||||
problems.push.apply(problems, sanitized.problems);
|
||||
if (validOps.length === 0) problems.push('No concrete, supported change was found.');
|
||||
|
||||
if (problems.length) {
|
||||
await addReaction('confused');
|
||||
await postComment(
|
||||
'🤔 **PocketBase AI Bot**: I need a bit more to act on that.\n\n- ' + problems.join('\n- ') +
|
||||
'\n\nTry naming the script and the exact change, e.g. `@pocketbase-bot set RAM to 4096 on zigbee2mqtt`.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 7. Propose (do NOT apply yet) ──────────────────────────────────
|
||||
const action = { slug: parsed.slug, operations: validOps };
|
||||
const bullets = validOps.map(function (op) {
|
||||
if (op.kind === 'field') return '- `' + op.field + '` → `' + JSON.stringify(op.value) + '`';
|
||||
if (op.kind === 'note') return '- note ' + op.action + ' `' + op.type + '`' + (op.text ? ': ' + op.text : '');
|
||||
return '- method ' + op.action + ' `' + op.type + '`: ' + JSON.stringify(op.changes);
|
||||
}).join('\n');
|
||||
const marker = '<!-- pocketbase-pending: ' + Buffer.from(JSON.stringify(action), 'utf8').toString('base64') + ' -->';
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'🤖 **PocketBase AI Bot** — please confirm\n\n' +
|
||||
(parsed.human_summary ? '> ' + parsed.human_summary + '\n\n' : '') +
|
||||
'**Target:** `' + action.slug + '`\n**Proposed changes:**\n' + bullets + '\n\n' +
|
||||
'Reply **`@pocketbase-bot confirm`** to apply, or restate the request to adjust.\n' + marker
|
||||
);
|
||||
})().catch(function (e) {
|
||||
console.error('Fatal error:', e && (e.message || e));
|
||||
process.exit(1);
|
||||
});
|
||||
ENDSCRIPT
|
||||
Generated
-5
@@ -343,7 +343,6 @@ 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) ' +
|
||||
@@ -545,7 +544,6 @@ 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');
|
||||
@@ -556,7 +554,6 @@ 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 + '`');
|
||||
@@ -988,8 +985,6 @@ jobs:
|
||||
logo: 'string',
|
||||
documentation: 'string',
|
||||
website: 'string',
|
||||
oidc_url: 'string',
|
||||
license: 'string',
|
||||
project_url: 'string',
|
||||
repository: 'string',
|
||||
config_path: 'string',
|
||||
|
||||
Generated
-780
@@ -1,780 +0,0 @@
|
||||
name: PR script test
|
||||
|
||||
# Builds the ct/ and vm/ scripts a pull request touches on real Proxmox and Incus hosts and
|
||||
# reports the outcome on the PR, as a check run and as one comment kept up to
|
||||
# date.
|
||||
#
|
||||
# Every push to a non-draft PR is built automatically, forks included: the
|
||||
# maintainers decided (2026-11-12) that the lab hosts run contributors' code
|
||||
# unattended. They are isolated, hold no secret, and are treated as
|
||||
# compromised anyway; the sweeper and fresh guests per build keep them usable.
|
||||
# Someone with write access can also comment `/test` (optionally followed by
|
||||
# a full SHA to pin, the scripts to build, and the platforms to build on) to
|
||||
# build again or pick what to build. Only a commit that existed when the
|
||||
# comment was written gets built: a push after it is never covered, not even
|
||||
# one that lands in the seconds before this job runs.
|
||||
#
|
||||
# Approval stays narrow: only PRs the push-app-to-main app opens on a branch
|
||||
# of this repository (a script moving over from DevScripts, reviewed and
|
||||
# tested there) are approved when every script passes. Every other PR gets
|
||||
# the report, the check, and "changes requested" on a real failure.
|
||||
#
|
||||
# gate ubuntu-latest who asked, which scripts, which commit
|
||||
# test proxmox-test / build + checks, one matrix job per script, platform
|
||||
# incus-test / (pve, incus, arm) and OS; arm runs on GitHub's
|
||||
# ubuntu-24.04-arm arm64 runner and is information only
|
||||
# report ubuntu-latest check run + PR comment
|
||||
#
|
||||
# The test job gets a read-only token and never sees a secret: the host it runs
|
||||
# on executes untrusted code and has to be treated as compromised.
|
||||
#
|
||||
# This file is identical in DevScripts and ProxmoxVE; github.repository decides
|
||||
# which URLs get rewritten and which container ID range is used.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# Every PR, see above. pull_request_target runs this file from main, never
|
||||
# the PR's copy of it, and does not wait for "approve and run" on forks.
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- "ct/**"
|
||||
- "install/**"
|
||||
- "vm/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: "Pull request number"
|
||||
required: true
|
||||
type: number
|
||||
sha:
|
||||
description: "Pin a commit (full 40-character SHA); empty = the latest, if nothing was pushed since"
|
||||
required: false
|
||||
type: string
|
||||
scripts:
|
||||
description: "Space-separated app names, ct/ or vm/ paths, and platforms (pve, incus, arm); empty = everything the PR touches, everywhere"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
# One queue per PR. A newer push replaces a run that is still waiting (its
|
||||
# gate would skip anyway: the head moved), but never kills one that is
|
||||
# building: a cancelled job leaves its guest behind until the sweeper. Any
|
||||
# comment starts this workflow; one that is not a /test gets a group of its
|
||||
# own, so it cannot push a waiting test out of the queue.
|
||||
concurrency:
|
||||
group: >-
|
||||
pr-script-test-${{ github.event_name == 'issue_comment' && !startsWith(github.event.comment.body, '/test')
|
||||
&& format('noop-{0}', github.run_id)
|
||||
|| github.event.pull_request.number || github.event.issue.number || inputs.pr }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/test')) ||
|
||||
(github.event_name == 'pull_request_target' && !github.event.pull_request.draft)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read # when the run was dispatched, for a /test without SHA
|
||||
checks: write
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
pr: ${{ steps.plan.outputs.pr }}
|
||||
repo: ${{ steps.plan.outputs.repo }}
|
||||
sha: ${{ steps.plan.outputs.sha }}
|
||||
check_id: ${{ steps.plan.outputs.check_id }}
|
||||
actor: ${{ steps.plan.outputs.actor }}
|
||||
not_built: ${{ steps.plan.outputs.not_built }}
|
||||
trusted: ${{ steps.plan.outputs.trusted }}
|
||||
kind: ${{ steps.plan.outputs.kind }}
|
||||
platforms: ${{ steps.plan.outputs.platforms }}
|
||||
steps:
|
||||
- id: plan
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const MAX = 10;
|
||||
core.setOutput('matrix', '[]');
|
||||
|
||||
// Trusted: a PR the push-app-to-main app opened on a branch of this
|
||||
// repository, i.e. a script moving over from DevScripts.
|
||||
const TRUSTED_AUTHOR = 'push-app-to-main[bot]';
|
||||
const isTrusted = p => !!p && p.user && p.user.login === TRUSTED_AUTHOR && p.user.type === 'Bot' &&
|
||||
!!p.head.repo && p.head.repo.full_name === `${owner}/${repo}`;
|
||||
|
||||
let prNumber, requested = '';
|
||||
if (context.eventName === 'pull_request_target') {
|
||||
const p = context.payload.pull_request;
|
||||
prNumber = p.number;
|
||||
// Exactly the pushed commit; a later push starts its own run.
|
||||
requested = p.head.sha;
|
||||
} else if (context.eventName === 'workflow_dispatch') {
|
||||
prNumber = Number(context.payload.inputs.pr);
|
||||
requested = `${context.payload.inputs.sha || ''} ${context.payload.inputs.scripts || ''}`;
|
||||
} else {
|
||||
prNumber = context.payload.issue.number;
|
||||
const body = context.payload.comment.body.trim();
|
||||
if (!/^\/test(\s|$)/.test(body)) return;
|
||||
// author_association hides private org members, so ask for the permission itself.
|
||||
const user = context.payload.comment.user.login;
|
||||
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: user });
|
||||
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
|
||||
core.notice(`${user} has ${perm.permission} access; /test needs write`);
|
||||
return;
|
||||
}
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner, repo, comment_id: context.payload.comment.id, content: 'rocket',
|
||||
});
|
||||
requested = body.split('\n')[0].slice('/test'.length);
|
||||
}
|
||||
|
||||
// `/test <app>` in an issue (not a PR) builds that app from the
|
||||
// default branch as it is right now, e.g. to see whether a reported
|
||||
// error reproduces. No PR, so no check run, no review, no approval.
|
||||
const isIssue = context.eventName === 'issue_comment' && !context.payload.issue.pull_request;
|
||||
let pr = null, head, headSha;
|
||||
if (isIssue) {
|
||||
const { data: info } = await github.rest.repos.get({ owner, repo });
|
||||
const { data: branch } = await github.rest.repos.getBranch({ owner, repo, branch: info.default_branch });
|
||||
headSha = branch.commit.sha;
|
||||
head = { owner: { login: owner }, name: repo, full_name: `${owner}/${repo}` };
|
||||
} else {
|
||||
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }));
|
||||
if (pr.state !== 'open' || !pr.head.repo) { core.notice('PR is closed or its fork is gone'); return; }
|
||||
head = pr.head.repo;
|
||||
headSha = pr.head.sha;
|
||||
if (context.eventName === 'pull_request_target' && headSha !== requested) {
|
||||
core.notice('Pushed to again since; the newer push has its own run');
|
||||
return;
|
||||
}
|
||||
}
|
||||
core.setOutput('kind', isIssue ? 'issue' : 'pr');
|
||||
core.setOutput('trusted', !isIssue && isTrusted(pr) ? 'true' : 'false');
|
||||
const MARK = '<!-- pr-script-test -->';
|
||||
const sticky = async body => {
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber });
|
||||
// Only our own comment: anyone can paste the marker into theirs.
|
||||
const mine = comments.find(c => c.body.includes(MARK) && c.user.type === 'Bot');
|
||||
if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
|
||||
else await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
|
||||
};
|
||||
|
||||
// Which commit the approval covers. Reading pr.head.sha alone would
|
||||
// build whatever was pushed between the comment and this job
|
||||
// starting, i.e. code nobody looked at. Two ways to be sure:
|
||||
//
|
||||
// /test <sha> that exact commit, and only while it is the head. A
|
||||
// full SHA: the author writes both commits, so a short
|
||||
// prefix is a quick birthday collision away.
|
||||
// /test the latest commit, if nothing reached the fork after
|
||||
// the comment. pushed_at is set by GitHub on every push
|
||||
// and read after the PR, so a push in between shows up.
|
||||
// Branches of this repository belong to people with
|
||||
// write access, who could build anything anyway.
|
||||
const words = requested.split(/\s+/).filter(Boolean);
|
||||
// Platform words pick where to build: `/test arm`, `/test pve incus
|
||||
// bark-server`. None means every platform. A run on only some of
|
||||
// them is reported under its own check name and never approves.
|
||||
const PLATFORM = { pve: 'pve', proxmox: 'pve', incus: 'incus', arm: 'arm', arm64: 'arm', aarch64: 'arm' };
|
||||
const platformOf = t => PLATFORM[t.toLowerCase()];
|
||||
const only = new Set(words.map(platformOf).filter(Boolean));
|
||||
const onlyList = ['pve', 'incus', 'arm'].filter(p => only.has(p));
|
||||
const tokens = words.filter(t => !platformOf(t));
|
||||
const isSha = t => /^[0-9a-f]{7,40}$/.test(t);
|
||||
// App names (bark-server) become ct/ paths; such a name without a
|
||||
// ct/ script falls back to vm/ below.
|
||||
const bare = new Set();
|
||||
const toPath = t => {
|
||||
if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(t) || !/^[A-Za-z0-9._-]+$/.test(t)) return t;
|
||||
const p = `ct/${t.replace(/\.sh$/, '')}.sh`;
|
||||
bare.add(p);
|
||||
return p;
|
||||
};
|
||||
const approved = isIssue ? [] : tokens.filter(isSha);
|
||||
let entries = (isIssue ? tokens : tokens.filter(t => !isSha(t))).map(toPath);
|
||||
const refuse = async (why, retry) => {
|
||||
await sticky([MARK, '### Proxmox script test', '', `⚠️ ${why} Nothing was built.`, '', retry].join('\n'));
|
||||
core.notice(why);
|
||||
};
|
||||
if (isIssue) {
|
||||
// The default branch is trusted; only which app is needed.
|
||||
if (entries.length === 0) {
|
||||
await refuse('In an issue, `/test` needs to know which app.', 'Comment `/test <app>`, for example `/test bark-server` (the name of its `ct/` script).');
|
||||
return;
|
||||
}
|
||||
} else if (approved.length > 1) {
|
||||
await refuse('`/test` takes at most one commit.', 'Comment `/test` to build the latest commit.');
|
||||
return;
|
||||
}
|
||||
if (approved.length === 1 && approved[0] !== headSha) {
|
||||
await refuse(
|
||||
approved[0].length !== 40 && headSha.startsWith(approved[0])
|
||||
? 'A short SHA is not enough, it has to be all 40 characters.'
|
||||
: `\`${approved[0]}\` is not the head of this PR (any more).`,
|
||||
'Comment `/test` to build the latest commit.');
|
||||
return;
|
||||
}
|
||||
if (!isIssue && approved.length === 0 && head.full_name !== `${owner}/${repo}`) {
|
||||
const askedAt = context.eventName === 'workflow_dispatch'
|
||||
? (await github.rest.actions.getWorkflowRun({ owner, repo, run_id: context.runId })).data.created_at
|
||||
: context.payload.comment.created_at;
|
||||
const { data: fork } = await github.rest.repos.get({ owner: head.owner.login, repo: head.name });
|
||||
if (new Date(fork.pushed_at) >= new Date(askedAt)) {
|
||||
await refuse(
|
||||
`Something was pushed to ${head.full_name} after the \`/test\`, so it is not certain which commit was meant.`,
|
||||
'Comment `/test` again once you have looked at the latest commit.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (entries.some(e => !/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(e))) {
|
||||
await refuse(
|
||||
isIssue ? 'Only app names (as in `ct/<app>.sh` or `vm/<name>.sh`) and platforms (`pve`, `incus`, `arm`) can be requested.' : 'Only app names, `ct/<name>.sh` and `vm/<name>.sh` paths, platforms (`pve`, `incus`, `arm`) and the commit can be requested.',
|
||||
isIssue ? 'For example: `/test bark-server`, `/test arm bark-server` or `/test debian-vm`' : 'For example: `/test`, `/test arm`, or `/test pve incus bark-server`.');
|
||||
return;
|
||||
}
|
||||
// Everything the PR changes that this test cannot build is named in
|
||||
// the comment, so a /test never ends in silence.
|
||||
const notBuilt = [];
|
||||
const code = s => '`' + String(s).replace(/[`\r\n]/g, '') + '`';
|
||||
if (entries.length === 0) {
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: prNumber });
|
||||
const set = new Set();
|
||||
for (const f of files) {
|
||||
if (f.status === 'removed') continue;
|
||||
let m;
|
||||
if ((m = f.filename.match(/^install\/([A-Za-z0-9._-]+)-install\.sh$/))) set.add(`ct/${m[1]}.sh`);
|
||||
else if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(f.filename)) set.add(f.filename);
|
||||
else if (/^(tools|turnkey)\/.*\.sh$/.test(f.filename)) notBuilt.push(`${code(f.filename)}: only \`ct/\` and \`vm/\` scripts are built`);
|
||||
}
|
||||
entries = [...set].sort();
|
||||
}
|
||||
|
||||
const include = [];
|
||||
const fetchScript = async p => {
|
||||
try {
|
||||
const res = await github.rest.repos.getContent({ owner: head.owner.login, repo: head.name, path: p, ref: headSha });
|
||||
return Buffer.from(res.data.content || '', 'base64').toString('utf8');
|
||||
} catch (e) { return null; }
|
||||
};
|
||||
for (let script of entries.slice(0, MAX)) {
|
||||
let text = await fetchScript(script);
|
||||
// /test debian-vm in an issue: there is no ct/debian-vm.sh, but a vm/ one.
|
||||
const wasBare = isIssue || bare.has(script);
|
||||
if (text === null && wasBare && script.startsWith('ct/')) {
|
||||
const vm = `vm/${script.slice(3)}`;
|
||||
if ((text = await fetchScript(vm)) !== null) script = vm;
|
||||
}
|
||||
if (text === null) {
|
||||
const where = isIssue ? 'on the default branch' : 'in this PR';
|
||||
notBuilt.push(wasBare
|
||||
? (script.startsWith('ct/') ? `${code(script.slice(3, -3))}: neither \`ct/\` nor \`vm/\` has it ${where}` : `${code(script)} does not exist ${where}`)
|
||||
: `\`${script}\` does not exist in this PR (an \`install/\` script is built through the \`ct/\` script of the same name)`);
|
||||
continue;
|
||||
}
|
||||
const isVm = script.startsWith('vm/');
|
||||
// The test hosts are amd64; an ARM-only VM script refuses to run there.
|
||||
if (isVm && /^\s*vm_require_arch\s+arm64\b/m.test(text)) {
|
||||
notBuilt.push(`${code(script)}: ARM64 only, the test hosts are amd64`);
|
||||
continue;
|
||||
}
|
||||
// Scripts that ask for the OS get built once per OS.
|
||||
const oses = !isVm && /msg_menu\s+"Choose the container OS"/.test(text) ? ['debian', 'alpine'] : [''];
|
||||
const name = isVm ? `vm-${script.slice(3, -3)}` : script.slice(3, -3);
|
||||
// Every ct/ script on every platform: the same files run on Proxmox
|
||||
// and, through community-scripts/core's Incus backend, on Incus.
|
||||
// VMs are Proxmox only. ARM (Incus on GitHub's arm64 runner) is
|
||||
// information only and never decides the result; a script that
|
||||
// says var_arm64=no is not built there. Unset is built with
|
||||
// var_arm64=yes, to see whether it would work.
|
||||
const arm = (text.match(/^[ \t]*var_arm64="\$\{var_arm64:-(yes|no)\}"/m) || [])[1] || 'unset';
|
||||
let platforms = isVm ? ['pve'] : arm === 'no' ? ['pve', 'incus'] : ['pve', 'incus', 'arm'];
|
||||
if (!isVm && arm === 'no' && (!only.size || only.has('arm'))) notBuilt.push(`${code(script)} on ARM64: the script sets \`var_arm64=no\``);
|
||||
if (only.size) {
|
||||
if (isVm && !only.has('pve')) notBuilt.push(`${code(script)}: VM scripts are built on \`pve\` only`);
|
||||
platforms = platforms.filter(p => only.has(p));
|
||||
}
|
||||
for (const platform of platforms) {
|
||||
for (const os of oses) include.push({ script, os, platform, arm64: platform === 'arm' ? arm : '', name: `${os ? `${name}-${os}` : name}-${platform}` });
|
||||
}
|
||||
}
|
||||
for (const script of entries.slice(MAX)) notBuilt.push(`\`${script}\`: at most ${MAX} scripts per run, name it with \`/test ${script}\``);
|
||||
if (include.length === 0) {
|
||||
await sticky([
|
||||
MARK, '### Proxmox script test', '',
|
||||
isIssue || onlyList.length
|
||||
? `ℹ️ Nothing to build${onlyList.length ? ` on ${onlyList.join(', ')}` : ''}:`
|
||||
: 'ℹ️ Nothing to build: this test builds `ct/` and `vm/` scripts (and changed `install/` scripts through their `ct/` script).', '',
|
||||
...(notBuilt.length ? notBuilt.map(n => `- ${n}`) : ['- this PR changes no `ct/`, `vm/` or `install/` script']),
|
||||
].join('\n'));
|
||||
core.notice('No ct/ or vm/ script to build');
|
||||
return;
|
||||
}
|
||||
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
// A check run belongs to a PR's commit; an issue has none.
|
||||
let checkId = '';
|
||||
if (!isIssue) {
|
||||
const { data: check } = await github.rest.checks.create({
|
||||
// A partial run gets its own name: it must not stand in for the
|
||||
// full test, e.g. as a required check.
|
||||
owner, repo, name: `Proxmox script test${onlyList.length ? ` (${onlyList.join(', ')} only)` : ''}`, head_sha: headSha, status: 'in_progress',
|
||||
details_url: runUrl,
|
||||
output: { title: `Building ${include.length}`, summary: include.map(i => `- \`${i.script}\` ${[i.platform, i.os].filter(Boolean).join(' · ')}`).join('\n') },
|
||||
});
|
||||
checkId = String(check.id);
|
||||
}
|
||||
|
||||
core.setOutput('matrix', JSON.stringify(include));
|
||||
core.setOutput('pr', String(prNumber));
|
||||
core.setOutput('repo', head.full_name);
|
||||
core.setOutput('sha', headSha);
|
||||
core.setOutput('check_id', checkId);
|
||||
core.setOutput('not_built', JSON.stringify(notBuilt));
|
||||
core.setOutput('platforms', onlyList.join(' '));
|
||||
// Who asked: the /test commenter, or whoever dispatched the run.
|
||||
core.setOutput('actor', context.actor);
|
||||
|
||||
await sticky([
|
||||
MARK, '### Proxmox script test', '',
|
||||
`⏳ Building ${include.length} at ${headSha.slice(0, 7)}${isIssue ? ' (default branch)' : ''}${onlyList.length ? `, only on ${onlyList.join(', ')}` : ''} — [watch it live](${runUrl}) (open the \`test\` job)`, '',
|
||||
...include.map(i => `- \`${i.script}\` (${[i.platform, i.os].filter(Boolean).join(' · ')})`),
|
||||
...(notBuilt.length ? ['', '**Not built:**', ...notBuilt.map(n => `- ${n}`)] : []),
|
||||
].join('\n'));
|
||||
|
||||
test:
|
||||
needs: gate
|
||||
if: needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
|
||||
# Only `proxmox-test` / `incus-test`, never `self-hosted`: the runners are
|
||||
# registered without default labels so that no other workflow can be
|
||||
# scheduled onto them. ARM runs on GitHub's own arm64 runner, a fresh VM
|
||||
# per job (free for public repositories).
|
||||
runs-on: ${{ matrix.platform == 'arm' && 'ubuntu-24.04-arm' || matrix.platform == 'incus' && 'incus-test' || 'proxmox-test' }}
|
||||
# ARM is information only: a failed or missing ARM job (no runner, no
|
||||
# minutes left, ...) never turns the run red. The report leaves it out of
|
||||
# the result as well.
|
||||
continue-on-error: ${{ matrix.platform == 'arm' }}
|
||||
permissions:
|
||||
contents: read
|
||||
timeout-minutes: 90 # install (45) + update (30) + checks
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.gate.outputs.matrix) }}
|
||||
name: test (${{ matrix.name }})
|
||||
steps:
|
||||
# The default branch, never the PR: the harness itself must be trusted.
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
sparse-checkout: .github/workflows/scripts
|
||||
persist-credentials: false
|
||||
|
||||
# The arm64 runner is a plain Ubuntu VM: Incus and the screenshot
|
||||
# browser are set up on it for this one job.
|
||||
- name: Set up the ARM runner
|
||||
if: matrix.platform == 'arm'
|
||||
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
|
||||
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
|
||||
|
||||
- name: Build and check ${{ matrix.script }}
|
||||
env:
|
||||
PR_REPO: ${{ needs.gate.outputs.repo }}
|
||||
PR_SHA: ${{ needs.gate.outputs.sha }}
|
||||
SCRIPT: ${{ matrix.script }}
|
||||
OS: ${{ matrix.os }}
|
||||
# ARM is the Incus harness on another machine.
|
||||
PLATFORM: ${{ matrix.platform == 'arm' && 'incus' || matrix.platform }}
|
||||
VAR_ARM64: ${{ matrix.platform == 'arm' && 'yes' || '' }}
|
||||
UPSTREAM: ${{ github.repository }}
|
||||
CTID_BASE: ${{ github.repository == 'community-scripts/ProxmoxVE' && '9050' || '9000' }}
|
||||
# Only so release lookups are not rate limited: unauthenticated, all
|
||||
# test containers share the lab's one public IP and 60 calls an hour.
|
||||
# This job's token can read public contents and expires with the job.
|
||||
GH_API_TOKEN: ${{ github.token }}
|
||||
OUT_DIR: ${{ runner.temp }}/result
|
||||
# The self-hosted runners run as root; the GitHub one needs sudo (pct
|
||||
# and incus do).
|
||||
run: >-
|
||||
${{ matrix.platform == 'arm' && 'sudo --preserve-env=PR_REPO,PR_SHA,SCRIPT,OS,PLATFORM,VAR_ARM64,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT' || '' }}
|
||||
bash .github/workflows/scripts/pve-run-script.sh
|
||||
|
||||
- if: always() && matrix.platform == 'arm'
|
||||
run: sudo chmod -R a+rX "$RUNNER_TEMP/result" 2>/dev/null || true
|
||||
|
||||
- if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: result-${{ matrix.name }}
|
||||
path: ${{ runner.temp }}/result
|
||||
if-no-files-found: ignore
|
||||
retention-days: 14
|
||||
|
||||
report:
|
||||
needs: [gate, test]
|
||||
if: always() && needs.gate.result == 'success' && needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
checks: write
|
||||
contents: write # the ci-screenshots branch only; main is protected
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: result-*
|
||||
path: results
|
||||
|
||||
- uses: actions/github-script@v8
|
||||
env:
|
||||
MATRIX: ${{ needs.gate.outputs.matrix }}
|
||||
PR: ${{ needs.gate.outputs.pr }}
|
||||
SHA: ${{ needs.gate.outputs.sha }}
|
||||
CHECK_ID: ${{ needs.gate.outputs.check_id }}
|
||||
TEST_RESULT: ${{ needs.test.result }}
|
||||
ACTOR: ${{ needs.gate.outputs.actor }}
|
||||
NOT_BUILT: ${{ needs.gate.outputs.not_built }}
|
||||
TRUSTED: ${{ needs.gate.outputs.trusted }}
|
||||
KIND: ${{ needs.gate.outputs.kind }}
|
||||
PLATFORMS: ${{ needs.gate.outputs.platforms }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const { owner, repo } = context.repo;
|
||||
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
|
||||
const matrix = JSON.parse(process.env.MATRIX);
|
||||
|
||||
// download-artifact unpacks a single artifact straight into results/
|
||||
// and several into results/<name>/, so look for every result.json
|
||||
// and match it by what it says it is.
|
||||
const path = require('path');
|
||||
const found = new Map();
|
||||
const walk = dir => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(p);
|
||||
else if (e.name === 'result.json') {
|
||||
try {
|
||||
const r = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
// Written on the host that ran the PR's code: anything but a
|
||||
// known status is an error, never something new.
|
||||
if (!['pass', 'fail', 'unsupported', 'error'].includes(r.status)) r.status = 'error';
|
||||
const log = path.join(dir, 'install.log');
|
||||
r.install_log = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
|
||||
const shot = path.join(dir, 'screenshot.png');
|
||||
r.shot_file = fs.existsSync(shot) ? shot : '';
|
||||
if (!['updated', 'no-update', 'fail', 'error', ''].includes(r.update_status || '')) r.update_status = '';
|
||||
// Installed fine, but the update could not be tested (e.g. a
|
||||
// GitHub rate limit): not a pass, and not the PR's fault.
|
||||
if (r.status === 'pass' && r.update_status === 'error') { r.status = 'error'; r.step = `update ${r.update_step}`; }
|
||||
const ulog = path.join(dir, 'update.log');
|
||||
r.update_log = fs.existsSync(ulog) ? fs.readFileSync(ulog, 'utf8') : '';
|
||||
const ushot = path.join(dir, 'screenshot-update.png');
|
||||
r.shot_update_file = fs.existsSync(ushot) ? ushot : '';
|
||||
// ARM runs the Incus harness on an arm64 machine; result.json
|
||||
// says so in arch. Only the platforms this workflow knows: an
|
||||
// unknown one would never match a matrix row anyway.
|
||||
if (r.arch === 'aarch64') r.platform = 'arm';
|
||||
else if (!['pve', 'incus'].includes(r.platform)) r.platform = 'pve';
|
||||
found.set(`${r.script}|${r.platform}|${r.os}`, r);
|
||||
} catch (err) { core.warning(`unreadable ${p}: ${err.message}`); }
|
||||
}
|
||||
}
|
||||
};
|
||||
walk('results');
|
||||
|
||||
// A job that died before writing result.json still gets a row.
|
||||
const rows = matrix.map(m => ({ ...(found.get(`${m.script}|${m.platform || 'pve'}|${m.os || 'default'}`) || {
|
||||
script: m.script, os: m.os || 'default', platform: m.platform || 'pve',
|
||||
status: process.env.TEST_RESULT === 'cancelled' ? 'cancelled' : 'error',
|
||||
step: 'no result (runner or job failure)', duration: 0, log_tail: '', install_log: '', shot_file: '',
|
||||
update_status: '', update_log: '', shot_update_file: '',
|
||||
}), name: m.name }));
|
||||
// Where a row ran: "pve", "incus · alpine", "arm64", or "VM" (Proxmox only).
|
||||
const isVm = r => r.script.startsWith('vm/');
|
||||
const isArm = r => r.platform === 'arm';
|
||||
const where = r => {
|
||||
if (isVm(r)) return 'VM';
|
||||
const p = isArm(r) ? 'arm64' : r.platform;
|
||||
return r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
|
||||
};
|
||||
// ARM is information only: shown, never counted. The result, the
|
||||
// check, the review and the approval come from Proxmox and Incus.
|
||||
const coreRows = rows.filter(r => !isArm(r));
|
||||
const armRows = rows.filter(isArm);
|
||||
|
||||
// Screenshots go to an orphan branch, because a comment can only show
|
||||
// an image that has a URL. One file per PR and script, overwritten on
|
||||
// every run; the link names the commit, so no cache shows an old one.
|
||||
// The PNG comes from the host that ran the PR's code: check it again.
|
||||
const BRANCH = 'ci-screenshots';
|
||||
const prNum = Number(process.env.PR);
|
||||
const isPng = buf => buf.length > 8 && buf.length < 3000000 &&
|
||||
buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
const ensureBranch = async () => {
|
||||
try { await github.rest.git.getRef({ owner, repo, ref: `heads/${BRANCH}` }); return; }
|
||||
catch (e) { if (e.status !== 404) throw e; }
|
||||
const { data: blob } = await github.rest.git.createBlob({ owner, repo, encoding: 'utf-8',
|
||||
content: 'Screenshots taken by pr-script-test.yml for the PR comments. Not code; never merge this branch.\n' });
|
||||
const { data: tree } = await github.rest.git.createTree({ owner, repo, tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: blob.sha }] });
|
||||
const { data: commit } = await github.rest.git.createCommit({ owner, repo, message: 'Start ci-screenshots', tree: tree.sha, parents: [] });
|
||||
await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${BRANCH}`, sha: commit.sha });
|
||||
};
|
||||
const uploadShot = async (r, local, suffix) => {
|
||||
const buf = fs.readFileSync(local);
|
||||
if (!isPng(buf)) { core.warning(`${r.script}: screenshot is not a plausible PNG, skipped`); return ''; }
|
||||
const file = `${process.env.KIND === 'issue' ? 'issue' : 'pr'}-${prNum}/${r.name}${suffix}.png`;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
await ensureBranch();
|
||||
let sha;
|
||||
try { sha = (await github.rest.repos.getContent({ owner, repo, path: file, ref: BRANCH })).data.sha; }
|
||||
catch (e) { if (e.status !== 404) throw e; }
|
||||
const { data } = await github.rest.repos.createOrUpdateFileContents({
|
||||
owner, repo, branch: BRANCH, path: file, sha, content: buf.toString('base64'),
|
||||
message: `Screenshot: ${r.script} (${where(r)})${suffix} for #${prNum} at ${process.env.SHA.slice(0, 7)}`,
|
||||
});
|
||||
return `https://raw.githubusercontent.com/${owner}/${repo}/${data.commit.sha}/${file}`;
|
||||
} catch (e) {
|
||||
// Two reports at once race on the branch head; try again.
|
||||
core.warning(`${r.script}: screenshot upload attempt ${attempt} failed: ${e.message}`);
|
||||
await new Promise(res => setTimeout(res, 2000 * attempt));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
for (const r of rows) {
|
||||
if (r.shot_file) r.shot_url = await uploadShot(r, r.shot_file, '');
|
||||
if (r.shot_update_file) r.shot_update_url = await uploadShot(r, r.shot_update_file, '-after-update');
|
||||
}
|
||||
|
||||
// unsupported: the script needs a CPU feature the test host lacks
|
||||
// (AVX2 and the like). Not the PR's fault, so not a failure.
|
||||
const icon = { pass: '✅', fail: '❌', error: '⚠️', cancelled: '⏹️', unsupported: '🖥️' };
|
||||
const dur = s => `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`;
|
||||
// Text from the build lands outside code blocks here: no table breaks,
|
||||
// no code spans, and no @mention of a stranger (zero-width space).
|
||||
const clean = s => String(s || '').replace(/[|\r\n]/g, ' ').replace(/`/g, "'").replace(/@/g, '@').slice(0, 200);
|
||||
const fence = s => String(s || '').replace(/~~~~/g, '~~~');
|
||||
|
||||
// fail: the app broke, the PR's business. error/cancelled: our test
|
||||
// setup did, not the PR's. unsupported: the host's CPU is too old.
|
||||
const count = s => coreRows.filter(r => r.status === s).length;
|
||||
const passed = count('pass');
|
||||
const untestable = count('unsupported');
|
||||
const broken = count('fail');
|
||||
const setupTrouble = count('error') + count('cancelled');
|
||||
const allPass = passed === coreRows.length;
|
||||
// `/test arm`: nothing that counts was built, so the result is
|
||||
// information only as a whole.
|
||||
const armOnly = coreRows.length === 0;
|
||||
// Built on only some platforms (`/test pve`): a pass there says
|
||||
// nothing about the others, so it neither approves nor lifts an
|
||||
// earlier "changes requested". A failure is still a failure.
|
||||
const partial = !!process.env.PLATFORMS;
|
||||
const cancelled = (armOnly ? rows : coreRows).every(r => r.status === 'cancelled');
|
||||
const of = `**${passed}/${coreRows.length} passed**`;
|
||||
const armPassed = armRows.filter(r => r.status === 'pass').length;
|
||||
const armLine = armRows.length && !armOnly
|
||||
? `ARM64 (information only, not counted): ${armPassed}/${armRows.length} passed${armPassed < armRows.length ? `; failures there do not block this PR` : ''}.`
|
||||
: '';
|
||||
const headline = armOnly
|
||||
? `ℹ️ ARM64 only, information only: **${armPassed}/${armRows.length} passed**`
|
||||
: broken
|
||||
? `❌ ${of}`
|
||||
: setupTrouble
|
||||
? `⚠️ ${of}, ${setupTrouble} could not be tested because of a problem with the test setup (not the PR)`
|
||||
: untestable
|
||||
? `🖥️ ${of}, ${untestable} need${untestable === 1 ? 's' : ''} a newer CPU than the test host has (nothing failed)`
|
||||
: `✅ ${of}`;
|
||||
|
||||
// Update: the installed app is set back a version and updated with
|
||||
// the PR's update_script. ➖ = it ran but had nothing to update.
|
||||
const updateIcon = { updated: '✅', 'no-update': '➖', fail: '❌', error: '⚠️' };
|
||||
const detail = r => {
|
||||
if (r.status !== 'pass') return clean(r.step);
|
||||
const ip = /^[0-9.]{7,15}$/.test(r.ip || '') ? r.ip : '';
|
||||
if (isVm(r) && !r.url) return ip ? `booted, got ${ip}` : 'booted, no DHCP lease (an installer ISO, or no network)';
|
||||
const reach = r.url ? `${clean(r.url)} → ${r.http_code}` : 'no URL to check';
|
||||
return r.update_status === 'no-update' ? `${reach}; update: nothing to update${r.update_step ? ` (${clean(r.update_step)})` : ''}` : reach;
|
||||
};
|
||||
const table = [
|
||||
'| Script | Platform | Install | Update | Duration | Detail |',
|
||||
'|---|---|---|---|---|---|',
|
||||
...rows.map(r => `| \`${r.script}\` | ${where(r)} | ${r.update_status ? '✅' : icon[r.status] || '❔'} | ${updateIcon[r.update_status] || '–'} | ${dur(r.duration || 0)} | ${detail(r)} |`),
|
||||
].join('\n');
|
||||
|
||||
// A comment holds 65536 characters. Each script gets an equal share
|
||||
// and a log that does not fit loses its beginning: the end is where
|
||||
// a failure shows.
|
||||
const budget = Math.floor(52000 / rows.length);
|
||||
const tail = (text, max) => {
|
||||
text = fence(text).trimEnd();
|
||||
if (text.length <= max) return text;
|
||||
const kept = text.slice(text.length - max);
|
||||
const cut = text.slice(0, text.length - max).split('\n').length;
|
||||
return `… ${cut} earlier lines cut, the full log is in the run artifacts\n${kept.slice(kept.indexOf('\n') + 1)}`;
|
||||
};
|
||||
const details = (summaryLine, text, max) => [
|
||||
`<details><summary>${summaryLine}</summary>`, '',
|
||||
'~~~~text', tail(text, max), '~~~~', '</details>',
|
||||
].join('\n');
|
||||
|
||||
const logs = rows.map(r => {
|
||||
const parts = [];
|
||||
const failed = r.status !== 'pass';
|
||||
// A VM script's own output is all there is: no install log inside.
|
||||
const buildShare = (failed || isVm(r)) && r.log_tail ? Math.min(r.log_tail.length, Math.floor(budget / 3)) : 0;
|
||||
if (buildShare) parts.push(details(`${icon[r.status] || '❔'} ${r.script} (${where(r)}) — build log`, r.log_tail, buildShare));
|
||||
if (isVm(r)) return parts.join('\n\n');
|
||||
// The update log gets up to a third of what is left, the install log the rest.
|
||||
const updateShare = r.update_log ? Math.min(r.update_log.length, Math.floor((budget - buildShare) / 3)) : 0;
|
||||
if (r.install_log) {
|
||||
const lines = r.install_log.trimEnd().split('\n').length;
|
||||
parts.push(details(`📄 ${r.script} (${where(r)}) — install log (${lines} lines)`, r.install_log, budget - buildShare - updateShare - 400));
|
||||
} else if (!buildShare) {
|
||||
parts.push(`<sub>${r.script} (${where(r)}): no install log was captured</sub>`);
|
||||
}
|
||||
if (updateShare) {
|
||||
const lines = r.update_log.trimEnd().split('\n').length;
|
||||
parts.push(details(`🔄 ${r.script} (${where(r)}) — update log (${lines} lines)`, r.update_log, updateShare));
|
||||
}
|
||||
return parts.join('\n\n');
|
||||
});
|
||||
|
||||
const shots = rows.filter(r => r.shot_url || r.shot_update_url).map(r => [
|
||||
`<details open><summary>📸 ${r.script} (${where(r)}) — ${r.url ? clean(r.url) : 'console'}${r.shot_url && r.shot_update_url ? ': after install / after update' : r.shot_update_url ? ': after update' : ''}</summary>`, '',
|
||||
[r.shot_url, r.shot_update_url].filter(Boolean)
|
||||
.map((u, i, all) => `<img src="${u}" width="${all.length > 1 ? 420 : 640}" alt="Screenshot of ${r.script}">`).join(' '),
|
||||
'', '</details>',
|
||||
].join('\n'));
|
||||
|
||||
const summary = [
|
||||
`${headline} at \`${process.env.SHA.slice(0, 7)}\``, '',
|
||||
table, '',
|
||||
...(armLine ? [`<sub>${armLine}</sub>`, ''] : []),
|
||||
...(partial ? [`<sub>Built only on ${process.env.PLATFORMS.split(' ').join(', ')}; comment \`/test\` for every platform.</sub>`, ''] : []),
|
||||
...shots, '',
|
||||
...(() => { try { const n = JSON.parse(process.env.NOT_BUILT || '[]'); return n.length ? ['**Not built:**', ...n.map(x => `- ${x}`), ''] : []; } catch (e) { return []; } })(),
|
||||
...logs, '',
|
||||
`Full logs: [workflow run](${runUrl}) (artifacts \`result-*\`). Comment \`/test${partial ? ` ${process.env.PLATFORMS}` : ''}${process.env.KIND === 'issue' ? ' ' + [...new Set(rows.map(r => r.script.slice(3, -3)))].join(' ') : ''}\` to build again.`,
|
||||
].join('\n');
|
||||
|
||||
// Issues have no check run (there is no PR commit to attach it to).
|
||||
if (process.env.CHECK_ID) {
|
||||
await github.rest.checks.update({
|
||||
owner, repo, check_run_id: Number(process.env.CHECK_ID), status: 'completed',
|
||||
// Untestable here is "test it by hand", not a pass: action_required,
|
||||
// because a required check would count neutral as passed.
|
||||
conclusion: cancelled ? 'cancelled' : armOnly ? 'neutral' : allPass ? 'success' : broken ? 'failure' : 'action_required',
|
||||
output: {
|
||||
title: armOnly
|
||||
? `ARM64 ${armPassed}/${armRows.length} passed (information only)`
|
||||
: `${passed}/${coreRows.length} passed${armRows.length ? ` (+ ARM ${armPassed}/${armRows.length}, not counted)` : ''}`,
|
||||
summary: summary.slice(0, 65000),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The result pings whoever started the test. A mention added by
|
||||
// editing a comment does not reliably notify, so the old comment
|
||||
// goes and the result is posted as a new one, at the end of the
|
||||
// thread where it is looked for.
|
||||
// Whoever started the test and the PR's author, once each; no bots.
|
||||
const pr = Number(process.env.PR);
|
||||
const isIssue = process.env.KIND === 'issue';
|
||||
const { data: prInfo } = isIssue
|
||||
? await github.rest.issues.get({ owner, repo, issue_number: pr })
|
||||
: await github.rest.pulls.get({ owner, repo, pull_number: pr });
|
||||
const author = prInfo.user && prInfo.user.type === 'User' ? prInfo.user.login : '';
|
||||
const login = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
|
||||
const ping = [...new Set([process.env.ACTOR || '', author])].filter(l => login.test(l)).map(l => `@${l} `).join('');
|
||||
const MARK = '<!-- pr-script-test -->';
|
||||
const body = `${MARK}\n### Proxmox script test\n\n${ping}${summary}`.slice(0, 65000);
|
||||
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr });
|
||||
for (const c of comments.filter(c => c.body.includes(MARK) && c.user.type === 'Bot')) {
|
||||
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id });
|
||||
}
|
||||
const { data: posted } = await github.rest.issues.createComment({ owner, repo, issue_number: pr, body });
|
||||
// An issue has nothing to review or approve: the comment is all.
|
||||
if (isIssue) return;
|
||||
|
||||
// A real failure requests changes on the tested commit. Untestable
|
||||
// (CPU), runner errors and cancelled runs are not the PR's fault and
|
||||
// leave the review state alone. Our previous "changes requested" is
|
||||
// always dismissed first: a pass lifts it, a new failure replaces it.
|
||||
const REVIEW_MARK = '<!-- pr-script-test-review -->';
|
||||
const failedRows = cancelled ? [] : coreRows.filter(r => r.status === 'fail');
|
||||
const sha7 = process.env.SHA.slice(0, 7);
|
||||
// Trusted PRs (scripts moving over from DevScripts) are approved when
|
||||
// every script passes -- not on untestable or setup trouble, which
|
||||
// means "not tested", not "good". That approval counts toward the
|
||||
// ruleset's required review; a push dismisses it as stale, and a
|
||||
// later run that does not fully pass withdraws it.
|
||||
// Anyone with write access can push to a migration branch, and the
|
||||
// approval counts toward the required review. So it only covers
|
||||
// what a migration contains: ct/ and install/ scripts. A PR that
|
||||
// also changes anything else (workflows, misc/, ...) is tested but
|
||||
// never approved.
|
||||
let approve = process.env.TRUSTED === 'true' && allPass && !partial;
|
||||
// A partial run that passed leaves every review as it is.
|
||||
const leaveReviews = partial && !failedRows.length;
|
||||
let noApproveWhy = '';
|
||||
if (approve) {
|
||||
const changed = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr });
|
||||
const other = changed.map(f => f.filename).filter(n => !/^(ct|install)\/[A-Za-z0-9._-]+\.sh$/.test(n));
|
||||
if (other.length) {
|
||||
approve = false;
|
||||
noApproveWhy = `Not approved automatically: besides \`ct/\` and \`install/\` scripts this PR changes ${other.slice(0, 5).map(n => '`' + n.replace(/[`\r\n]/g, '') + '`').join(', ')}${other.length > 5 ? ', …' : ''}. That needs a human review.`;
|
||||
}
|
||||
}
|
||||
if (noApproveWhy) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: posted.id, body: `${body}\n\n> ${noApproveWhy}`.slice(0, 65000) });
|
||||
}
|
||||
try {
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr });
|
||||
const ours = state => reviews.filter(v => v.user && v.user.type === 'Bot' && v.state === state && (v.body || '').includes(REVIEW_MARK));
|
||||
for (const v of leaveReviews ? [] : ours('CHANGES_REQUESTED')) {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner, repo, pull_number: pr, review_id: v.id,
|
||||
message: failedRows.length ? 'Replaced by a newer Proxmox script test.' : `Proxmox script test passed at ${sha7}.`,
|
||||
});
|
||||
}
|
||||
if (!approve && !cancelled && !leaveReviews) {
|
||||
for (const v of ours('APPROVED')) {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner, repo, pull_number: pr, review_id: v.id,
|
||||
message: `Withdrawn: the Proxmox script test at ${sha7} did not fully pass.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (approve && !ours('APPROVED').some(v => v.commit_id === process.env.SHA)) {
|
||||
await github.rest.pulls.createReview({
|
||||
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'APPROVE',
|
||||
body: [
|
||||
REVIEW_MARK, `**Proxmox script test passed** at \`${sha7}\`: ${rows.map(r => `\`${r.script}\``).join(', ')}.`, '',
|
||||
`Script moved over from DevScripts, built and checked on a real Proxmox host. [Test result](${posted.html_url}).`,
|
||||
].join('\n'),
|
||||
});
|
||||
}
|
||||
if (failedRows.length) {
|
||||
await github.rest.pulls.createReview({
|
||||
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'REQUEST_CHANGES',
|
||||
body: [
|
||||
REVIEW_MARK, `**Proxmox script test failed** at \`${sha7}\`:`, '',
|
||||
...failedRows.map(r => `- \`${r.script}\` (${where(r)}): ${clean(r.step)}`), '',
|
||||
`Logs and details: [test result](${posted.html_url}). Push a fix, then ask for \`/test\` again.`,
|
||||
].join('\n'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Could not update the review state: ${e.message}`);
|
||||
}
|
||||
Generated
+7
-63
@@ -2,8 +2,7 @@ name: PR test command
|
||||
|
||||
# Posts a ready-to-run test command for reviewers.
|
||||
# It uses this PR’s script branch with the production engine, since both resolve
|
||||
# independently. ct/ scripts need community-scripts/core; vm/, tools/ and
|
||||
# turnkey/ scripts run straight from the branch.
|
||||
# independently. Only scripts using community-scripts/core are supported.
|
||||
#
|
||||
# pull_request_target allows comments on fork PRs. No PR code is checked out or
|
||||
# executed; API inputs are validated and the comment is assembled in JavaScript.
|
||||
@@ -15,9 +14,6 @@ on:
|
||||
paths:
|
||||
- "ct/**"
|
||||
- "install/**"
|
||||
- "vm/**"
|
||||
- "tools/**"
|
||||
- "turnkey/**"
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
@@ -54,43 +50,14 @@ jobs:
|
||||
// ct/foo.sh and install/foo-install.sh are the same app. A removed
|
||||
// file has nothing left to run.
|
||||
const apps = new Map(); // slug -> {ct, install}
|
||||
const others = [];
|
||||
for (const f of files) {
|
||||
if (f.status === 'removed') continue;
|
||||
let m = f.filename.match(/^ct\/([a-z0-9][a-z0-9._-]*)\.sh$/);
|
||||
if (m) { apps.set(m[1], { ...apps.get(m[1]), ct: true }); continue; }
|
||||
m = f.filename.match(/^install\/([a-z0-9][a-z0-9._-]*)-install\.sh$/);
|
||||
if (m) { apps.set(m[1], { ...apps.get(m[1]), install: true }); continue; }
|
||||
if (/^(vm|tools|turnkey)\/[A-Za-z0-9._\/-]+\.sh$/.test(f.filename)) others.push(f.filename);
|
||||
if (m) apps.set(m[1], { ...apps.get(m[1]), install: true });
|
||||
}
|
||||
if (apps.size === 0 && others.length === 0) return;
|
||||
others.sort();
|
||||
|
||||
// Where a tool runs is read from the script itself; it is never run.
|
||||
const HOST = /(^|[\s;&|(`$])(pct|qm|pveam|pvesm|pvesh|pveversion)\s/m;
|
||||
async function runsOn(path) {
|
||||
if (path.startsWith('vm/')) return 'pve';
|
||||
const appliance = (path.split('/').pop().match(/(?:^|-)(pbs|pmg|pdm)(?=[0-9-]|\.sh$)/) || [])[1];
|
||||
if (path.startsWith('tools/pve/')) return appliance || 'pve';
|
||||
let text = '';
|
||||
try {
|
||||
const res = await github.rest.repos.getContent({
|
||||
owner: head.owner.login, repo: head.name, path, ref: pr.head.sha,
|
||||
});
|
||||
text = Buffer.from(res.data.content || '', 'base64').toString('utf8');
|
||||
} catch (e) {}
|
||||
if (HOST.test(text.replace(/^\s*#.*$/gm, '').replace(/command -v \S+/g, ''))) return 'pve';
|
||||
if (appliance) return appliance;
|
||||
return path.startsWith('tools/addon/') ? 'guest' : 'unknown';
|
||||
}
|
||||
const WHERE = {
|
||||
pve: 'in the Proxmox VE shell',
|
||||
guest: 'inside the LXC or VM that should get it',
|
||||
pbs: 'in the Proxmox Backup Server shell',
|
||||
pmg: 'in the Proxmox Mail Gateway shell',
|
||||
pdm: 'in the Proxmox Datacenter Manager shell',
|
||||
unknown: 'wherever the script is meant to run',
|
||||
};
|
||||
if (apps.size === 0) return;
|
||||
|
||||
// Read the ct script at the PR head to see which engine it loads.
|
||||
// Read only -- it is never sourced or run.
|
||||
@@ -144,9 +111,10 @@ jobs:
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
'Both lines are needed. Without `COMMUNITY_SCRIPTS_URL` the engine falls back to',
|
||||
'`main`, so curling the branch URL on its own gives you the `ct/` script from',
|
||||
'this PR and the `install/` script from `main`. Frequently the one you meant to test.',
|
||||
'Both lines are needed. Each script pins `_CS_DEFAULT_URL` to `main`, and that',
|
||||
'pin is what fills `COMMUNITY_SCRIPTS_URL` when the variable is unset — so',
|
||||
'curling the branch URL on its own gives you the `ct/` script from this PR and',
|
||||
'the `install/` script from `main`. Frequently the one you meant to test.',
|
||||
'',
|
||||
'The same command works on an Incus host: the engine detects the platform and',
|
||||
'loads the matching backend, while the scripts still come from this branch.',
|
||||
@@ -165,30 +133,6 @@ jobs:
|
||||
);
|
||||
}
|
||||
|
||||
if (others.length > 0) {
|
||||
const shown = others.slice(0, MAX_APPS);
|
||||
lines.push(...(ready.length > 0 ? ['', '---', ''] : ['### Try this branch', '']));
|
||||
for (const path of shown) {
|
||||
lines.push(
|
||||
`\`${path}\`, run ${WHERE[await runsOn(path)]}:`,
|
||||
'```bash',
|
||||
`bash -c "$(curl -fsSL "${base}/${path}")"`,
|
||||
'```',
|
||||
'',
|
||||
);
|
||||
}
|
||||
if (others.length > shown.length) {
|
||||
lines.push(
|
||||
`${others.length - shown.length} more script(s) changed; same command, different path.`,
|
||||
'',
|
||||
);
|
||||
}
|
||||
lines.push(
|
||||
'Only the script itself comes from this branch. The engine it loads still',
|
||||
'comes from community-scripts/core on `main`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (legacy.length > 0) {
|
||||
lines.push(
|
||||
'',
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
name: Request Script Eligibility Check
|
||||
|
||||
# category.slug is the same string GitHub uses in the "new discussion" URL
|
||||
# (?category=request-script, linked from every repo's issue chooser), so this
|
||||
# guard matches the live category without needing its numeric id.
|
||||
on:
|
||||
discussion:
|
||||
types: [created, edited, reopened]
|
||||
|
||||
jobs:
|
||||
check-eligibility:
|
||||
if: github.repository == 'community-scripts/ProxmoxVE' && github.event.discussion.category.slug == 'request-script'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
discussions: write
|
||||
issues: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Verify stars/age, flag or close
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const discussion = context.payload.discussion;
|
||||
|
||||
if (!discussion || discussion.state === "closed") {
|
||||
core.info("Discussion missing or already closed — skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
const body = discussion.body || "";
|
||||
const lines = body.split("\n");
|
||||
const fields = {};
|
||||
let current = null;
|
||||
for (const line of lines) {
|
||||
const m = line.match(/^###\s+(.+?)\s*$/);
|
||||
if (m) {
|
||||
current = m[1].trim();
|
||||
fields[current] = [];
|
||||
} else if (current) {
|
||||
fields[current].push(line);
|
||||
}
|
||||
}
|
||||
function fieldText(label) {
|
||||
const raw = (fields[label] || []).join("\n").trim();
|
||||
return raw === "_No response_" ? "" : raw;
|
||||
}
|
||||
|
||||
const sourceRepo = fieldText("Source Repository");
|
||||
const justification = fieldText("Justification for an exception");
|
||||
|
||||
let eligible = false;
|
||||
let verified = false;
|
||||
const ghMatch = sourceRepo.match(/^https?:\/\/(?:www\.)?github\.com\/([^\/\s]+)\/([^\/\s#?]+?)(?:\.git)?\/?(?:[#?].*)?$/i);
|
||||
|
||||
if (ghMatch) {
|
||||
const [, repoOwner, repoName] = ghMatch;
|
||||
try {
|
||||
const { data: repoData } = await github.rest.repos.get({ owner: repoOwner, repo: repoName });
|
||||
const stars = repoData.stargazers_count;
|
||||
const createdAt = new Date(repoData.created_at);
|
||||
const now = new Date();
|
||||
const ageMonths =
|
||||
(now.getFullYear() - createdAt.getFullYear()) * 12 +
|
||||
(now.getMonth() - createdAt.getMonth()) -
|
||||
(now.getDate() < createdAt.getDate() ? 1 : 0);
|
||||
eligible = stars >= 1000 && ageMonths >= 6;
|
||||
verified = true;
|
||||
core.info(`Verified ${repoOwner}/${repoName}: ${stars} stars, ${ageMonths} months old, eligible=${eligible}`);
|
||||
} catch (err) {
|
||||
core.warning(`Could not verify ${repoOwner}/${repoName}: ${err.message} — treating as unverifiable.`);
|
||||
}
|
||||
} else {
|
||||
core.info("Source Repository is not a github.com URL — cannot auto-verify, treating as unverifiable.");
|
||||
}
|
||||
|
||||
if (eligible) {
|
||||
core.info(`Discussion #${discussion.number} meets the requirements — no action.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = (discussion.labels || []).map(l => l.name);
|
||||
const reasonLine = verified
|
||||
? "the source repository does not meet the 1,000-star / 6-month minimum adoption requirement"
|
||||
: "the source repository's star count and age could not be verified automatically";
|
||||
|
||||
if (justification) {
|
||||
if (labels.includes("investigation")) {
|
||||
core.info(`Discussion #${discussion.number} already flagged for investigation — skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: "investigation",
|
||||
color: "fbca04",
|
||||
description: "Below the normal script-request threshold; a justification was given and needs maintainer review.",
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status !== 422) throw err;
|
||||
}
|
||||
|
||||
const labelQuery = await github.graphql(
|
||||
`query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { label(name: "investigation") { id } } }`,
|
||||
{ owner, repo }
|
||||
);
|
||||
const labelNodeId = labelQuery.repository.label.id;
|
||||
|
||||
await github.graphql(
|
||||
`mutation($labelableId: ID!, $labelIds: [ID!]!) { addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) { clientMutationId } }`,
|
||||
{ labelableId: discussion.node_id, labelIds: [labelNodeId] }
|
||||
);
|
||||
|
||||
await github.graphql(
|
||||
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
|
||||
{
|
||||
discussionId: discussion.node_id,
|
||||
body:
|
||||
`Thanks for the justification — ${reasonLine}, so this has been flagged \`investigation\` for a maintainer ` +
|
||||
`to review manually rather than closed automatically.`,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await github.graphql(
|
||||
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
|
||||
{
|
||||
discussionId: discussion.node_id,
|
||||
body:
|
||||
`This request doesn't meet the minimum requirements (1,000 stars, project at least 6 months old) — ` +
|
||||
`${reasonLine}, and no justification was given. Closing automatically.\n\n` +
|
||||
`If you believe this project qualifies for an exception, edit this discussion to fill in ` +
|
||||
`**Justification for an exception** and reopen it — it will then be flagged for manual review instead of closed again.`,
|
||||
}
|
||||
);
|
||||
|
||||
await github.graphql(
|
||||
`mutation($discussionId: ID!) { closeDiscussion(input: { discussionId: $discussionId, reason: OUTDATED }) { discussion { id } } }`,
|
||||
{ discussionId: discussion.node_id }
|
||||
);
|
||||
}
|
||||
-192
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Turns a GitHub-hosted arm64 runner (ubuntu-24.04-arm) into a throwaway Incus
|
||||
# test host for pve-run-script.sh (PLATFORM=incus): the same layout as
|
||||
# incus-ci-host-setup.sh, minus what a VM that lives for one job does not need
|
||||
# (services, sweeper, runner registration). Run with sudo.
|
||||
#
|
||||
# The runner is a fresh VM per job and is discarded afterwards; nothing here
|
||||
# has to be undone.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NET_ADDR=10.98.0.1
|
||||
NET_CIDR=10.98.0.0/24
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
start=$(date +%s)
|
||||
|
||||
# --- Incus ----------------------------------------------------------------------
|
||||
# Ubuntu 24.04's own incus is 6.0.0, which lacks `-f csv,noheader`; the engine
|
||||
# then finds no storage pool. Zabbly (the Incus maintainers' packages) has the
|
||||
# current 6.0 LTS, as Debian 13 does.
|
||||
mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
|
||||
# The key Zabbly publishes in its install instructions, not whatever the
|
||||
# server returns today.
|
||||
gpg --show-keys --with-colons /etc/apt/keyrings/zabbly.asc 2>/dev/null |
|
||||
awk -F: '/^fpr/ {print $10; exit}' | grep -qx 4EFC590696CB15B87C73A3AD82CC8797C838DCFD ||
|
||||
{ echo "::error::unexpected Zabbly signing key"; exit 1; }
|
||||
# Only the Incus packages may come from there; everything else stays Ubuntu's.
|
||||
printf '%s\n' 'Package: *' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 1' '' \
|
||||
'Package: incus* liblxc* lxcfs* qemu* edk2*' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 600' \
|
||||
>/etc/apt/preferences.d/zabbly
|
||||
printf '%s\n' 'Enabled: yes' 'Types: deb' 'URIs: https://pkgs.zabbly.com/incus/lts-6.0' \
|
||||
"Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")" 'Components: main' \
|
||||
"Architectures: $(dpkg --print-architecture)" 'Signed-By: /etc/apt/keyrings/zabbly.asc' \
|
||||
>/etc/apt/sources.list.d/zabbly-incus-lts-6.0.sources
|
||||
# Each slow step has its own limit, so a hang names itself in the log instead
|
||||
# of holding the PR report until the job times out.
|
||||
step() { local t=$1; shift; timeout "$t" "$@" || { echo "::error::'$*' failed or hung (limit ${t})"; return 1; }; }
|
||||
# What the runner's network looks like when apt can't get through.
|
||||
netdiag() {
|
||||
echo "::group::Network on this runner"
|
||||
resolvectl status 2>&1 | grep -E 'DNS Servers|Current DNS|resolv.conf mode' || cat /etc/resolv.conf
|
||||
local h
|
||||
for h in ports.ubuntu.com pkgs.zabbly.com github.com; do
|
||||
echo "$h -> $(timeout 10 getent ahostsv4 "$h" | awk '{print $1; exit}')"
|
||||
timeout 15 curl -s -o /dev/null -w " http %{http_code} in %{time_total}s\n" "http://$h/" || echo " no answer"
|
||||
done
|
||||
echo "::endgroup::"
|
||||
}
|
||||
# The runner's resolver (Azure's 168.63.129.16) stopped resolving the arm64
|
||||
# mirror on 2026-11-11 while every other name worked. Then the name is
|
||||
# looked up over DNS-over-HTTPS and pinned in /etc/hosts for this job.
|
||||
for h in ports.ubuntu.com; do
|
||||
timeout 10 getent ahostsv4 "$h" >/dev/null && continue
|
||||
ip=$(curl -fsS --max-time 10 -H 'accept: application/dns-json' "https://1.1.1.1/dns-query?name=$h&type=A" |
|
||||
jq -r '[.Answer[]? | select(.type == 1) | .data][0] // empty')
|
||||
if [[ "$ip" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
|
||||
echo "$ip $h" >>/etc/hosts
|
||||
echo "::warning::the runner's DNS can't resolve $h; using $ip from 1.1.1.1"
|
||||
fi
|
||||
done
|
||||
step 5m apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 update -qq || { netdiag; exit 1; }
|
||||
DEBIAN_FRONTEND=noninteractive step 10m apt-get install -y -qq incus jq >/dev/null
|
||||
|
||||
# Docker on the runner sets the FORWARD policy to DROP, which cuts the Incus
|
||||
# containers off from the internet.
|
||||
iptables -P FORWARD ACCEPT
|
||||
|
||||
step 2m incus admin init --preseed <<EOF
|
||||
networks:
|
||||
- name: incusbr0
|
||||
type: bridge
|
||||
config:
|
||||
ipv4.address: ${NET_ADDR}/24
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: none
|
||||
storage_pools:
|
||||
- name: default
|
||||
driver: dir
|
||||
profiles:
|
||||
- name: default
|
||||
devices:
|
||||
root:
|
||||
path: /
|
||||
pool: default
|
||||
type: disk
|
||||
eth0:
|
||||
name: eth0
|
||||
network: incusbr0
|
||||
type: nic
|
||||
EOF
|
||||
|
||||
# The PR tree server the harness expects (a service on the lab hosts).
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
nohup python3 -m http.server "$SERVE_PORT" --bind "$NET_ADDR" --directory "$SERVE_ROOT" >/tmp/ci-serve.log 2>&1 &
|
||||
echo "Incus $(incus --version) ready after $(($(date +%s) - start))s"
|
||||
|
||||
# --- Screenshots ----------------------------------------------------------------
|
||||
# Optional: without them the build is still tested. The harness takes one only
|
||||
# when `chromium` exists, the user ci-shot exists and the CI_SHOT chain keeps
|
||||
# that user off private addresses (here: the cloud metadata service at
|
||||
# 169.254.169.254 and the runner itself).
|
||||
shots() {
|
||||
useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
# Ubuntu's apt chromium is a snap wrapper; Playwright ships a plain arm64
|
||||
# build. Started directly with --screenshot, that build crashes on this
|
||||
# runner (sandbox or not), so `chromium` here is a stand-in that takes the
|
||||
# harness's arguments and drives the browser through Playwright instead.
|
||||
mkdir -p /opt/pw
|
||||
(cd /opt/pw && step 5m npm install --silent --no-audit --no-fund playwright-core@1.49.1 >/dev/null) || return 1
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers DEBIAN_FRONTEND=noninteractive \
|
||||
step 10m /opt/pw/node_modules/.bin/playwright-core install --with-deps chromium >/dev/null || return 1
|
||||
chmod -R a+rX /opt/pw /opt/pw-browsers
|
||||
cat >/opt/pw/shot.js <<'EOF'
|
||||
// node shot.js <url> <out.png> <width> <height>
|
||||
const { chromium } = require('/opt/pw/node_modules/playwright-core');
|
||||
(async () => {
|
||||
const [url, out, w, h] = process.argv.slice(2);
|
||||
const browser = await chromium.launch({ chromiumSandbox: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: Number(w), height: Number(h) }, ignoreHTTPSErrors: true });
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 30000 }).catch(() => {});
|
||||
await page.waitForTimeout(5000);
|
||||
await page.screenshot({ path: out });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch(e => { console.error(e.message); process.exit(1); });
|
||||
EOF
|
||||
cat >/usr/local/bin/chromium <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Stand-in for `chromium --headless ... --screenshot=<file> --window-size=W,H <url>`
|
||||
# on the ARM runner (see arm-runner-setup.sh). Other flags are ignored.
|
||||
url="" out="" w=1280 h=800
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--version) echo "Chromium (Playwright $(node -p "require('/opt/pw/node_modules/playwright-core/package.json').version"))"; exit 0 ;;
|
||||
--screenshot=*) out="${a#*=}" ;;
|
||||
--window-size=*) s="${a#*=}"; w="${s%,*}"; h="${s#*,}" ;;
|
||||
-*) ;;
|
||||
*) url="$a" ;;
|
||||
esac
|
||||
done
|
||||
[[ -n "$url" && -n "$out" ]] || { echo "usage: chromium --screenshot=<file> <url>" >&2; exit 2; }
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers exec node /opt/pw/shot.js "$url" "$out" "$w" "$h"
|
||||
EOF
|
||||
chmod 755 /usr/local/bin/chromium
|
||||
# Ubuntu 24.04 forbids unprivileged user namespaces through AppArmor, and
|
||||
# Chromium's sandbox needs them. The sandbox stays on; the restriction goes,
|
||||
# on a VM that exists for this one job.
|
||||
sysctl -qw kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true
|
||||
|
||||
local uid fw net ns
|
||||
uid=$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
$fw -w -N CI_SHOT
|
||||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d "$NET_ADDR" -j REJECT
|
||||
iptables -w -A CI_SHOT -d "$NET_CIDR" -j ACCEPT
|
||||
for ns in 127.0.0.53 $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /run/systemd/resolve/resolv.conf 2>/dev/null); do
|
||||
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
|
||||
# The same call the harness makes, on a page that needs no network.
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
chown ci-shot "$dir"
|
||||
# HOME: runuser keeps root's, which ci-shot cannot write (the harness does the same).
|
||||
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
|
||||
--no-first-run --window-size=320,200 --user-data-dir="$dir/profile" \
|
||||
--screenshot="$dir/shot.png" 'data:text/html,<h1>ci</h1>' &>"$dir/chromium.log" || true
|
||||
if [[ ! -s "$dir/shot.png" ]]; then
|
||||
echo "Chromium could not take a test screenshot:"
|
||||
head -n 15 "$dir/chromium.log"
|
||||
echo "userns: $(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>&1)"
|
||||
rm -rf -- "$dir"
|
||||
return 1
|
||||
fi
|
||||
rm -rf -- "$dir"
|
||||
echo "$(chromium --version) ready for screenshots"
|
||||
}
|
||||
shots || echo "::warning::no screenshots on this ARM run (the build is still tested)"
|
||||
echo "Runner ready after $(($(date +%s) - start))s"
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Function for generating Figlet headers
|
||||
generate_headers() {
|
||||
local base_dir=$1
|
||||
local target_subdir=$2
|
||||
local search_pattern=$3
|
||||
|
||||
local headers_dir="${base_dir}/headers"
|
||||
mkdir -p "$headers_dir"
|
||||
rm -f "$headers_dir"/*
|
||||
|
||||
# Recursive or non-recursive search
|
||||
if [[ "$search_pattern" == "**" ]]; then
|
||||
shopt -s globstar nullglob
|
||||
file_list=("${base_dir}"/**/*.sh)
|
||||
shopt -u globstar
|
||||
else
|
||||
file_list=("${base_dir}"/*.sh)
|
||||
fi
|
||||
|
||||
for script in "${file_list[@]}"; do
|
||||
[[ -f "$script" ]] || continue
|
||||
|
||||
# -m1: a script that sets APP twice (the second carrying ${var_version})
|
||||
# otherwise yields both lines, and the filename gets a newline in it.
|
||||
app_name=$(grep -m1 -oP '^APP="\K[^"]+' "$script" 2>/dev/null)
|
||||
if [[ -n "$app_name" ]]; then
|
||||
# core.func/vm-core.func look the header up as "${APP,,}" without spaces,
|
||||
# so the generated file has to be named after APP - not after the script.
|
||||
output_file="${headers_dir}/$(echo "${app_name,,}" | tr -d ' ')"
|
||||
figlet_output=$(figlet -w 500 -f slant "$app_name")
|
||||
if [[ -n "$figlet_output" ]]; then
|
||||
echo "$figlet_output" >"$output_file"
|
||||
echo "Generated: $output_file"
|
||||
else
|
||||
echo "Figlet failed for $app_name in $script"
|
||||
fi
|
||||
else
|
||||
echo "No APP name found in $script, skipping."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ct
|
||||
generate_headers "./ct" "headers" "*"
|
||||
|
||||
# tools (addon, pve, ...)
|
||||
generate_headers "./tools" "headers" "**"
|
||||
|
||||
# vm
|
||||
generate_headers "./vm" "headers" "*"
|
||||
|
||||
echo "Completed processing all sections."
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time setup of an Incus test host behind pr-script-test.yml: the same
|
||||
# ct/ scripts as on Proxmox, built through community-scripts/core's Incus
|
||||
# backend. Safe to re-run. Run as root on a disposable Debian 13 machine (it
|
||||
# builds pull request code as root).
|
||||
#
|
||||
# incus-ci-host-setup.sh host only
|
||||
# incus-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
NET_ADDR=10.98.0.1
|
||||
NET_CIDR=10.98.0.0/24
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq incus jq curl python3 iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
|
||||
|
||||
# Containers get a NATed bridge of their own (Incus runs DHCP and DNS on it)
|
||||
# and a plain directory pool on the root disk.
|
||||
if ! incus storage show default &>/dev/null; then
|
||||
incus admin init --preseed <<EOF
|
||||
networks:
|
||||
- name: incusbr0
|
||||
type: bridge
|
||||
config:
|
||||
ipv4.address: ${NET_ADDR}/24
|
||||
ipv4.nat: "true"
|
||||
ipv6.address: none
|
||||
storage_pools:
|
||||
- name: default
|
||||
driver: dir
|
||||
profiles:
|
||||
- name: default
|
||||
devices:
|
||||
root:
|
||||
path: /
|
||||
pool: default
|
||||
type: disk
|
||||
eth0:
|
||||
name: eth0
|
||||
network: incusbr0
|
||||
type: nic
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Screenshots of the app under test are taken as this user, never as root.
|
||||
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
|
||||
# Same rules as on the Proxmox hosts: the screenshot browser may reach the
|
||||
# test containers, DNS and the public internet, nothing private, and not this
|
||||
# host's own address on the container network (the PR tree server).
|
||||
cat >/usr/local/sbin/ci-shot-firewall <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uid=\$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
\$fw -w -N CI_SHOT 2>/dev/null || \$fw -w -F CI_SHOT
|
||||
\$fw -w -C OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT 2>/dev/null ||
|
||||
\$fw -w -I OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d ${NET_ADDR} -j REJECT
|
||||
iptables -w -A CI_SHOT -d ${NET_CIDR} -j ACCEPT
|
||||
# systemd-resolved's stub; it forwards to the lab resolver itself.
|
||||
iptables -w -A CI_SHOT -d 127.0.0.53 -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d 127.0.0.53 -p tcp --dport 53 -j ACCEPT
|
||||
for ns in \$(awk '/^nameserver/ && \$2 !~ /:/ {print \$2}' /etc/resolv.conf /run/systemd/resolve/resolv.conf 2>/dev/null); do
|
||||
iptables -w -A CI_SHOT -d "\$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "\$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "\$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "\$net" -j REJECT
|
||||
done
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-shot-firewall
|
||||
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Keep the screenshot browser away from this host and the lab network
|
||||
After=network-online.target incus.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/sbin/ci-shot-firewall
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
[[ -s /etc/timezone ]] || {
|
||||
timedatectl set-timezone Europe/Vienna
|
||||
echo Europe/Vienna >/etc/timezone
|
||||
}
|
||||
|
||||
# The rewritten PR trees, served to the engine and to the containers. Waits
|
||||
# for incusbr0, which only exists once Incus is up.
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
cat >/etc/systemd/system/ci-serve.service <<EOF
|
||||
[Unit]
|
||||
Description=Serve pull request trees to script builds
|
||||
After=incus.service network-online.target
|
||||
Wants=incus.service
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/bin/sh -c 'for i in \$(seq 1 60); do ip -4 addr show dev incusbr0 | grep -q ${NET_ADDR} && exit 0; incus network list >/dev/null 2>&1; sleep 1; done; exit 1'
|
||||
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind ${NET_ADDR} --directory ${SERVE_ROOT}
|
||||
DynamicUser=yes
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Leftovers of a job that was killed before its cleanup trap ran.
|
||||
cat >/usr/local/sbin/ci-sweep <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Delete CI containers (ci-9000 .. ci-9099) that no running job holds, and stale trees.
|
||||
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
|
||||
# restart, timeout) never removes it, so a lock older than any job can run
|
||||
# (90 minutes) holds nothing.
|
||||
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
|
||||
for name in $(incus list -c n -f csv 2>/dev/null | grep -E '^ci-9[0-9]{3}$'); do
|
||||
held "$name" && continue
|
||||
incus delete "$name" --force &>/dev/null && echo "swept $name"
|
||||
done
|
||||
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
|
||||
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
|
||||
find /usr/local/community-scripts/logs /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
|
||||
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
|
||||
true
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-sweep
|
||||
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ci-sweep
|
||||
EOF
|
||||
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers every 3 hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=0/3:30
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
|
||||
systemctl restart ci-serve.service ci-shot-firewall.service
|
||||
|
||||
# --- Runner (one per repository) ----------------------------------------------
|
||||
if [[ $# -eq 2 ]]; then
|
||||
repo="$1"
|
||||
token="$2"
|
||||
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
|
||||
dir="/opt/actions-runner/${repo#*/}"
|
||||
mkdir -p "$dir"
|
||||
cd "$dir"
|
||||
if [[ ! -x ./config.sh ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
if [[ -f .runner ]]; then
|
||||
./svc.sh stop >/dev/null 2>&1 || true
|
||||
./svc.sh uninstall >/dev/null 2>&1 || true
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Only `incus-test`: see pve-ci-host-setup.sh for why there are no default labels.
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
|
||||
--url "https://github.com/${repo}" --token "$token" \
|
||||
--name "$(hostname -s)-${repo#*/}" --labels incus-test --work _work
|
||||
./svc.sh install root >/dev/null
|
||||
./svc.sh start >/dev/null
|
||||
echo "runner for ${repo} registered"
|
||||
fi
|
||||
|
||||
echo "incus test host ready: ${SERVE_ROOT} on ${NET_ADDR}:${SERVE_PORT}, sweeper every 3h"
|
||||
-220
@@ -1,220 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time setup of the Proxmox test host behind pr-script-test.yml.
|
||||
# Safe to re-run. Run as root on the test host (a disposable PVE, never a
|
||||
# production node: it builds pull request code as root).
|
||||
#
|
||||
# pve-ci-host-setup.sh host only
|
||||
# pve-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
|
||||
SERVE_ROOT=/srv/ci
|
||||
SERVE_PORT=18080
|
||||
UPLINK="${UPLINK:-vmbr0}"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq jq curl python3 dnsmasq iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
|
||||
|
||||
# Screenshots of the app under test are taken as this user, never as root.
|
||||
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
|
||||
|
||||
# The page Chromium renders is served by code under test, and a redirect or an
|
||||
# <img> could point it at this host (the PVE UI on :8006) or the lab network;
|
||||
# the screenshot would then publish that on the PR. So ci-shot may reach the
|
||||
# test containers, DNS and the public internet (apps load fonts and scripts
|
||||
# from CDNs), and nothing private.
|
||||
cat >/usr/local/sbin/ci-shot-firewall <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
uid=$(id -u ci-shot)
|
||||
for fw in iptables ip6tables; do
|
||||
$fw -w -N CI_SHOT 2>/dev/null || $fw -w -F CI_SHOT
|
||||
$fw -w -C OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT 2>/dev/null ||
|
||||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
|
||||
done
|
||||
iptables -w -A CI_SHOT -d 10.99.0.1 -j REJECT
|
||||
iptables -w -A CI_SHOT -d 10.99.0.0/24 -j ACCEPT
|
||||
for ns in $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /etc/resolv.conf); do
|
||||
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
|
||||
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
|
||||
done
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
|
||||
iptables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
|
||||
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
|
||||
done
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-shot-firewall
|
||||
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Keep the screenshot browser away from this host and the lab network
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/sbin/ci-shot-firewall
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Containers get their own NATed bridge instead of the lab network: no DHCP
|
||||
# lease surprises (a lab lease once handed a container the host's own address),
|
||||
# and an app listening on 8080 can never be confused with this host.
|
||||
cat >/etc/network/interfaces.d/ci-nat <<EOF
|
||||
auto vmbr1
|
||||
iface vmbr1 inet static
|
||||
address 10.99.0.1/24
|
||||
bridge-ports none
|
||||
bridge-stp off
|
||||
bridge-fd 0
|
||||
post-up sysctl -qw net.ipv4.ip_forward=1
|
||||
post-up iptables -t nat -C POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE
|
||||
post-down iptables -t nat -D POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE || true
|
||||
EOF
|
||||
ifreload -a
|
||||
cat >/etc/dnsmasq.d/ci.conf <<'EOF'
|
||||
interface=vmbr1
|
||||
bind-interfaces
|
||||
except-interface=lo
|
||||
dhcp-range=10.99.0.100,10.99.0.250,255.255.255.0,2h
|
||||
dhcp-option=option:router,10.99.0.1
|
||||
dhcp-option=option:dns-server,10.99.0.1
|
||||
no-resolv
|
||||
EOF
|
||||
# Upstream = whatever this host resolves with. Debian's dnsmasq would otherwise
|
||||
# ask resolvconf, which has nothing to offer on a PVE host.
|
||||
awk '/^nameserver/ {print "server=" $2}' /etc/resolv.conf >>/etc/dnsmasq.d/ci.conf
|
||||
systemctl enable dnsmasq >/dev/null
|
||||
systemctl restart dnsmasq
|
||||
|
||||
# A PVE ISO install writes /etc/timezone; a cloud-image install does not, and
|
||||
# the engine's pct_timezone aborts without it.
|
||||
[[ -s /etc/timezone ]] || {
|
||||
timedatectl set-timezone Europe/Vienna
|
||||
echo Europe/Vienna >/etc/timezone
|
||||
}
|
||||
|
||||
# Containers and VM disks live on the root filesystem; there is no LVM in a
|
||||
# cloud-image PVE. Snippets hold the Cloud-Init configs some vm/ scripts write.
|
||||
pvesm set local --content iso,vztmpl,backup,rootdir,images,snippets
|
||||
|
||||
# Templates are fetched once here rather than on every first build.
|
||||
pveam update >/dev/null
|
||||
for t in $(pveam available --section system | awk '/_amd64\./ && /debian-13-standard|alpine-3\.2[0-9]-default|ubuntu-24\.04-standard/ {print $2}'); do
|
||||
pveam list local | grep -q "$t" || pveam download local "$t" >/dev/null
|
||||
done
|
||||
|
||||
# The rewritten PR trees, served to the engine and to the containers.
|
||||
mkdir -p "$SERVE_ROOT"
|
||||
cat >/etc/systemd/system/ci-serve.service <<EOF
|
||||
[Unit]
|
||||
Description=Serve pull request trees to script builds
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind 10.99.0.1 --directory ${SERVE_ROOT}
|
||||
DynamicUser=yes
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Leftovers of a job that was killed before its cleanup trap ran.
|
||||
cat >/usr/local/sbin/ci-sweep <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Destroy CI containers (9000-9099) and VMs (9100-9199) that no running job
|
||||
# holds, and stale trees.
|
||||
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
|
||||
# restart, timeout) never removes it, so a lock older than any job can run
|
||||
# (90 minutes) holds nothing.
|
||||
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
|
||||
for id in $(pct list | awk 'NR>1 && $1>=9000 && $1<=9099 {print $1}'); do
|
||||
held "$id" && continue
|
||||
pct stop "$id" --skiplock 1 &>/dev/null || true
|
||||
pct destroy "$id" --purge 1 --force 1 &>/dev/null && echo "swept CT $id"
|
||||
done
|
||||
for id in $(qm list | awk 'NR>1 && $1>=9100 && $1<=9199 {print $1}'); do
|
||||
held "$id" && continue
|
||||
qm stop "$id" --skiplock 1 --timeout 30 &>/dev/null || true
|
||||
qm destroy "$id" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null && echo "swept VM $id"
|
||||
done
|
||||
# VM images and ISOs are cached between builds; drop the ones no build used
|
||||
# for a week. Container templates (*.tar.*) stay.
|
||||
find /var/lib/vz/template/cache /var/lib/vz/template/iso -maxdepth 1 -type f ! -name '*.tar.*' -atime +7 -delete 2>/dev/null
|
||||
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
|
||||
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
|
||||
# Engine logs that a killed job never got to remove.
|
||||
find /tmp -maxdepth 1 -type f \( -name 'create-lxc-*.log' -o -name 'pct_create_*.log' \
|
||||
-o -regex '/tmp/.*-9[0-9][0-9][0-9]-[0-9a-f]+\.log' \) -mmin +180 -delete
|
||||
find /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
|
||||
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
|
||||
true
|
||||
EOF
|
||||
chmod +x /usr/local/sbin/ci-sweep
|
||||
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ci-sweep
|
||||
EOF
|
||||
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
|
||||
[Unit]
|
||||
Description=Remove leftover CI containers every 3 hours
|
||||
|
||||
[Timer]
|
||||
OnCalendar=0/3:30
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
|
||||
systemctl restart ci-serve.service ci-shot-firewall.service
|
||||
|
||||
# --- Runner (one per repository) ----------------------------------------------
|
||||
if [[ $# -eq 2 ]]; then
|
||||
repo="$1"
|
||||
token="$2"
|
||||
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
|
||||
dir="/opt/actions-runner/${repo#*/}"
|
||||
mkdir -p "$dir"
|
||||
cd "$dir"
|
||||
if [[ ! -x ./config.sh ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
# A registered runner is reconfigured from scratch: stop and remove the old
|
||||
# registration (the token works for removal too).
|
||||
if [[ -f .runner ]]; then
|
||||
./svc.sh stop >/dev/null 2>&1 || true
|
||||
./svc.sh uninstall >/dev/null 2>&1 || true
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
|
||||
fi
|
||||
# Root, because pct needs it; the host is disposable by design.
|
||||
# --no-default-labels: without it the runner also answers to `self-hosted`,
|
||||
# and any other workflow with `runs-on: self-hosted` (and its secrets) could
|
||||
# land on this host.
|
||||
# Named after this host: with --replace, a fixed name would make a second
|
||||
# test host take over the first one's registration instead of adding to it.
|
||||
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
|
||||
--url "https://github.com/${repo}" --token "$token" \
|
||||
--name "$(hostname -s)-${repo#*/}" --labels proxmox-test --work _work
|
||||
./svc.sh install root >/dev/null
|
||||
./svc.sh start >/dev/null
|
||||
echo "runner for ${repo} registered"
|
||||
fi
|
||||
|
||||
echo "test host ready: ${SERVE_ROOT} on :${SERVE_PORT}, sweeper every 3h"
|
||||
-643
@@ -1,643 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build one ct/ or vm/ script from a pull request on this test host, check
|
||||
# that the container or VM really works, destroy it, and leave a result.json
|
||||
# behind. The vm/ part lives in pve-run-vm.sh, sourced from here.
|
||||
#
|
||||
# PLATFORM says what this host is: pve (Proxmox VE, pct) or incus. The ct/
|
||||
# scripts are the same for both; community-scripts/core picks its Incus
|
||||
# backend by itself when it finds incus instead of pveversion. Only the way
|
||||
# this harness reaches into the container differs (guest_* below).
|
||||
#
|
||||
# Runs on the self-hosted runner, which *is* the disposable test host. The
|
||||
# harness comes from the default branch; only the script under test comes from
|
||||
# the pull request, pinned to the commit a maintainer approved.
|
||||
#
|
||||
# The engine hardcodes raw.githubusercontent.com/<upstream>/main for every fetch
|
||||
# (ct -> build.func -> install script -> install.func -> tools.func). Pointing it
|
||||
# at a fork branch therefore means rewriting those URLs: the PR tree is unpacked
|
||||
# under SERVE_ROOT, every upstream main URL in it is replaced by this host's own
|
||||
# HTTP server, and the containers fetch from there. Engine changes in the PR are
|
||||
# tested along with the script, which is what a reviewer wants anyway.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
: "${PR_REPO:?}" "${PR_SHA:?}" "${SCRIPT:?}" "${UPSTREAM:?}" "${CTID_BASE:?}" "${OUT_DIR:?}"
|
||||
OS="${OS:-}"
|
||||
PLATFORM="${PLATFORM:-pve}"
|
||||
CTID_COUNT="${CTID_COUNT:-50}"
|
||||
SERVE_ROOT="${SERVE_ROOT:-/srv/ci}"
|
||||
# The containers' own network: vmbr1 + dnsmasq on Proxmox, incusbr0 on Incus.
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
SERVE_URL="${SERVE_URL:-http://10.98.0.1:18080}"
|
||||
BRIDGE="${BRIDGE:-incusbr0}"
|
||||
else
|
||||
SERVE_URL="${SERVE_URL:-http://10.99.0.1:18080}"
|
||||
BRIDGE="${BRIDGE:-vmbr1}"
|
||||
fi
|
||||
BUILD_TIMEOUT="${BUILD_TIMEOUT:-45m}"
|
||||
URL_WAIT="${URL_WAIT:-120}"
|
||||
|
||||
# Every value below reaches a path, a URL or a command line.
|
||||
[[ "$PR_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad PR_REPO" >&2; exit 2; }
|
||||
[[ "$UPSTREAM" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad UPSTREAM" >&2; exit 2; }
|
||||
[[ "$PR_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "bad PR_SHA" >&2; exit 2; }
|
||||
[[ "$SCRIPT" =~ ^(ct|vm)/[A-Za-z0-9._-]+\.sh$ ]] || { echo "bad SCRIPT" >&2; exit 2; }
|
||||
[[ -z "$OS" || "$OS" =~ ^(debian|ubuntu|alpine)$ ]] || { echo "bad OS" >&2; exit 2; }
|
||||
[[ "$PLATFORM" =~ ^(pve|incus)$ ]] || { echo "bad PLATFORM" >&2; exit 2; }
|
||||
# yes: build with var_arm64=yes. On arm64 the engine refuses to guess for a
|
||||
# script that leaves var_arm64 unset; the ARM run builds those anyway, to see.
|
||||
VAR_ARM64="${VAR_ARM64:-}"
|
||||
[[ -z "$VAR_ARM64" || "$VAR_ARM64" == "yes" ]] || { echo "bad VAR_ARM64" >&2; exit 2; }
|
||||
[[ "$PLATFORM" == "pve" || "$SCRIPT" == ct/* ]] || { echo "vm/ scripts run on pve only" >&2; exit 2; }
|
||||
[[ "$CTID_BASE" =~ ^[0-9]+$ ]] || { echo "bad CTID_BASE" >&2; exit 2; }
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
RAW_LOG="$OUT_DIR/build.raw.log"
|
||||
LOG="$OUT_DIR/build.log"
|
||||
RESULT="$OUT_DIR/result.json"
|
||||
INSTALL_LOG_OUT="$OUT_DIR/install.log"
|
||||
UPDATE_RAW="$OUT_DIR/update.raw.log"
|
||||
UPDATE_LOG="$OUT_DIR/update.log"
|
||||
UPDATE_TIMEOUT="${UPDATE_TIMEOUT:-30m}"
|
||||
|
||||
kind="${SCRIPT%%/*}" # ct or vm
|
||||
app=$(basename "$SCRIPT" .sh)
|
||||
job="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}-${PLATFORM}-${kind}-${app}-${OS:-default}"
|
||||
tree="$SERVE_ROOT/$job"
|
||||
base="$SERVE_URL/$job"
|
||||
started=$(date +%s)
|
||||
|
||||
status="fail"
|
||||
step=""
|
||||
exit_code=""
|
||||
ctid="" # the guest's ID (pve: 9000, incus: ci-9000), container or VM
|
||||
|
||||
# The container, whatever the platform. ctid is set once a slot is reserved.
|
||||
guest_exec() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then incus exec "$ctid" -- "$@"; else pct exec "$ctid" -- "$@"; fi
|
||||
}
|
||||
guest_exists() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then incus info "$ctid" &>/dev/null; else pct status "$ctid" &>/dev/null; fi
|
||||
}
|
||||
guest_running() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
[[ "$(incus list "^${ctid}\$" -c s -f csv 2>/dev/null)" == "RUNNING" ]]
|
||||
else
|
||||
pct status "$ctid" 2>/dev/null | grep -q running
|
||||
fi
|
||||
}
|
||||
guest_destroy() {
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
incus delete "$ctid" --force &>/dev/null || true
|
||||
else
|
||||
pct stop "$ctid" --skiplock 1 &>/dev/null || true
|
||||
pct destroy "$ctid" --purge 1 --force 1 &>/dev/null || true
|
||||
fi
|
||||
}
|
||||
ct_ip=""
|
||||
url=""
|
||||
http_code=""
|
||||
failed_units=""
|
||||
# updated: a real update ran; no-update: update ran, nothing to update;
|
||||
# fail: the update broke; empty: not attempted (the install failed).
|
||||
update_status=""
|
||||
update_step=""
|
||||
|
||||
# Spinner frames: the engine redraws "⠋ Starting VM" a few times a second
|
||||
# without a tty too. An alternation, not a bracket: these are multibyte and
|
||||
# the runner's locale may be C.
|
||||
SPINNER_RE='^\s*(⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏)'
|
||||
|
||||
strip_ansi() {
|
||||
sed -E 's/\x1B\[[0-9;?]*[A-Za-z]//g; s/\x1B[()][A-Z0-9]//g; s/\r/\n/g' | grep -vE '^\s*$' | grep -vE "$SPINNER_RE"
|
||||
}
|
||||
|
||||
# The install script's own output: everything $STD hides from the build log.
|
||||
# Both engines write it inside the container as /root/.install-<session>.log;
|
||||
# dev_mode=keep makes sure a failed container is still there to read it from.
|
||||
# If it is gone anyway, the copy the engine leaves on the host is the fallback.
|
||||
collect_install_log() {
|
||||
[[ -n "$ctid" ]] || return 0
|
||||
# Exactly one build ran in this container, so the newest install log is ours.
|
||||
if guest_exists &&
|
||||
guest_exec sh -c 'cat "$(ls -t /root/.install-*.log 2>/dev/null | head -n1)"' 2>/dev/null |
|
||||
strip_ansi >"$INSTALL_LOG_OUT" && [[ -s "$INSTALL_LOG_OUT" ]]; then
|
||||
return 0
|
||||
fi
|
||||
[[ "$PLATFORM" == "pve" ]] || { rm -f "$INSTALL_LOG_OUT"; return 0; }
|
||||
# The engine names its host-side files after the session: pct_create_<ctid>_<date>_<session>.log
|
||||
local sid f
|
||||
sid=$(find /tmp -maxdepth 1 -name "pct_create_${ctid}_*.log" -newer "$RAW_LOG.start" -printf '%f\n' 2>/dev/null |
|
||||
sed -E 's/.*_([0-9a-f]+)\.log$/\1/' | head -n1)
|
||||
[[ -n "$sid" ]] || { rm -f "$INSTALL_LOG_OUT"; return 0; }
|
||||
for f in /tmp/*-"${ctid}"-"${sid}".log /var/log/community-scripts/install-"${sid}"-combined-*.log; do
|
||||
[[ -s "$f" ]] && { strip_ansi <"$f" >"$INSTALL_LOG_OUT"; return 0; }
|
||||
done
|
||||
rm -f "$INSTALL_LOG_OUT"
|
||||
}
|
||||
|
||||
# "Requires AVX2" and friends: the script may be fine, the test host's CPU is
|
||||
# not (Xeon E5-2650, Sandy Bridge: AVX, no AVX2). Shown as untestable rather
|
||||
# than as a failure of the PR -- but the log is written by the code under test,
|
||||
# so its word is not enough. Only the engine's error lines count, only when the
|
||||
# build or install itself failed, and the feature they name must really be
|
||||
# missing from this host's /proc/cpuinfo. Claiming a feature the host has
|
||||
# changes nothing.
|
||||
CPU_RE='(requir|need)[a-z]*[^.]{0,60}(AVX(2|-?512)?|SSE4[._]?2|x86-64-v[234])\b|\b(AVX(2|-?512)?|x86-64-v[234])\b[^.]{0,60}(not (supported|available)|does not have|is missing|unsupported)'
|
||||
|
||||
classify_cpu_failure() {
|
||||
local line feature flags
|
||||
line=$(strip_ansi <"$RAW_LOG" | grep -E '✖|\[ERROR\]' | grep -m1 -iE "$CPU_RE" |
|
||||
sed -E 's/^\s*(✖️?|\[[^]]*\]|\s)*//' | cut -c1-200)
|
||||
[[ -n "$line" ]] || return 0
|
||||
# The CPU flag that would have to be missing for the message to be true.
|
||||
case "${line,,}" in
|
||||
*avx-512* | *avx512* | *x86-64-v4*) feature=avx512f ;;
|
||||
*avx2* | *x86-64-v3*) feature=avx2 ;;
|
||||
*sse4.2* | *sse4_2* | *x86-64-v2*) feature=sse4_2 ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
flags=" $(grep -m1 '^flags' /proc/cpuinfo | cut -d: -f2) "
|
||||
[[ "$flags" == *" ${feature} "* ]] && return 0
|
||||
status="unsupported"
|
||||
step="needs a newer CPU than the test host has (no ${feature}): ${line}"
|
||||
}
|
||||
|
||||
finish() {
|
||||
[[ -f "$RAW_LOG" ]] && strip_ansi <"$RAW_LOG" >"$LOG"
|
||||
[[ -f "$RAW_LOG" ]] && collect_install_log
|
||||
[[ -f "$UPDATE_RAW" ]] && strip_ansi <"$UPDATE_RAW" >"$UPDATE_LOG"
|
||||
rm -f "$UPDATE_RAW"
|
||||
jq -n \
|
||||
--arg script "$SCRIPT" --arg kind "$kind" --arg platform "$PLATFORM" --arg os "${OS:-default}" --arg status "$status" \
|
||||
--arg step "$step" --arg exit_code "$exit_code" --arg sha "$PR_SHA" \
|
||||
--arg ctid "$ctid" --arg url "$url" --arg http_code "$http_code" \
|
||||
--arg failed_units "$failed_units" --arg ip "$ct_ip" --arg arch "$(uname -m)" \
|
||||
--arg update_status "$update_status" --arg update_step "$update_step" \
|
||||
--argjson duration "$(($(date +%s) - started))" \
|
||||
--arg tail "$([[ -f "$LOG" ]] && tail -n 60 "$LOG")" \
|
||||
'{script: $script, kind: $kind, platform: $platform, arch: $arch, os: $os, status: $status, step: $step, exit_code: $exit_code,
|
||||
sha: $sha, ctid: $ctid, ip: $ip, url: $url, http_code: $http_code,
|
||||
failed_units: $failed_units, update_status: $update_status, update_step: $update_step,
|
||||
duration: $duration, log_tail: $tail}' >"$RESULT"
|
||||
echo "::group::result"
|
||||
jq 'del(.log_tail)' "$RESULT"
|
||||
echo "::endgroup::"
|
||||
}
|
||||
|
||||
# --- Live output ------------------------------------------------------------------
|
||||
# The job log on GitHub streams while the job runs, so mirroring the build there
|
||||
# is all "live" takes. Two streams: the engine's own output, and the install
|
||||
# log inside the container, which holds everything $STD hides (apt, npm, ...).
|
||||
#
|
||||
# Both are written by the code under test. A line starting with `::` would be
|
||||
# read by the runner as a workflow command, so command processing is switched
|
||||
# off for as long as they run, with a token the build cannot know.
|
||||
stream_token=""
|
||||
log_pid=""
|
||||
install_pid=""
|
||||
|
||||
# Colours stay, cursor movement and spinner redraws go.
|
||||
live_filter() {
|
||||
sed -u -E 's/\r/\n/g; s/\x1B\[[0-9;?]*[ABCDEFGHJKSTfhilnsu]//g' | sed -u -E "/^\s*\$/d; /${SPINNER_RE}/d"
|
||||
}
|
||||
|
||||
start_streams() {
|
||||
stream_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
echo "::stop-commands::${stream_token}"
|
||||
tail -n +1 -F --pid="$build_pid" "$RAW_LOG" 2>/dev/null > >(live_filter) &
|
||||
log_pid=$!
|
||||
# A VM has no install log to follow: its script runs on this host only.
|
||||
[[ "$kind" == "ct" ]] || return 0
|
||||
# setsid: its own process group, so stop_streams can take down the
|
||||
# pct/incus exec + tail pair inside it in one go.
|
||||
setsid bash -c '
|
||||
gx() { if [ "$2" = incus ]; then incus exec "$1" -- "${@:3}"; else pct exec "$1" -- "${@:3}"; fi; }
|
||||
for _ in $(seq 1 900); do
|
||||
f=$(gx "$1" "$2" sh -c "ls -t /root/.install-*.log 2>/dev/null | head -n1" 2>/dev/null)
|
||||
if [ -n "$f" ]; then
|
||||
echo "──────── install log of CT $1 (live) ────────"
|
||||
if [ "$2" = incus ]; then exec incus exec "$1" -- tail -n +1 -F "$f"; fi
|
||||
exec pct exec "$1" -- tail -n +1 -F "$f"
|
||||
fi
|
||||
sleep 2
|
||||
done' _ "$ctid" "$PLATFORM" 2>/dev/null > >(live_filter | sed -u 's/^/ │ /') &
|
||||
install_pid=$!
|
||||
}
|
||||
|
||||
stop_streams() {
|
||||
[[ -n "$stream_token" ]] || return 0
|
||||
[[ -n "$install_pid" ]] && kill -TERM -- "-$install_pid" &>/dev/null
|
||||
[[ -n "$log_pid" ]] && kill -TERM "$log_pid" &>/dev/null
|
||||
wait "$log_pid" "$install_pid" &>/dev/null
|
||||
sleep 1 # let the filters flush
|
||||
install_pid=""
|
||||
log_pid=""
|
||||
echo "::${stream_token}::"
|
||||
stream_token=""
|
||||
}
|
||||
|
||||
build_pid=""
|
||||
update_pid=""
|
||||
cleanup() {
|
||||
stop_streams
|
||||
local pid
|
||||
for pid in "$build_pid" "$update_pid"; do
|
||||
[[ -n "$pid" ]] || continue
|
||||
pkill -TERM -P "$pid" &>/dev/null || true
|
||||
kill -TERM "$pid" &>/dev/null || true
|
||||
sleep 2
|
||||
done
|
||||
if [[ -n "$ctid" && "$kind" == "vm" ]]; then
|
||||
vm_cleanup
|
||||
elif [[ -n "$ctid" ]]; then
|
||||
guest_exists && guest_destroy
|
||||
rm -f "/run/ci-jobs/$ctid"
|
||||
# What the engine leaves on the host for this container and its session.
|
||||
local f sid
|
||||
[[ "$PLATFORM" == "pve" ]] && for f in /tmp/pct_create_"${ctid}"_*.log; do
|
||||
[[ -f "$f" && "$f" -nt "$RAW_LOG.start" ]] || continue
|
||||
sid=$(sed -E 's/.*_([0-9a-f]+)\.log$/\1/' <<<"$f")
|
||||
rm -f -- "$f" /tmp/create-lxc-"${sid}".log /tmp/*-"${ctid}"-"${sid}".log
|
||||
done
|
||||
fi
|
||||
rm -f "$RAW_LOG.start"
|
||||
rm -rf -- "$tree"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
# A cancelled job sends TERM; without this the EXIT trap would not run.
|
||||
trap 'exit 143' TERM
|
||||
trap 'exit 130' INT
|
||||
|
||||
# fail <reason> [fail|unsupported]: only the build/install branches pass the
|
||||
# second argument, after classify_cpu_failure has checked the host.
|
||||
#
|
||||
# The exit code says whether *this job* worked, not whether the app did. A
|
||||
# script that fails to build is a result: it is written down, reported on the
|
||||
# PR (check, comment, review) and the job exits 0 -- a red run per broken app
|
||||
# would only be notification noise. Only trouble with the test setup itself
|
||||
# (setup_error) fails the job, because that is ours to look at.
|
||||
fail() {
|
||||
step="$1"
|
||||
status="${2:-fail}"
|
||||
[[ "$status" == "unsupported" ]] || status="fail"
|
||||
finish
|
||||
echo "::warning title=${SCRIPT}::${step}"
|
||||
exit 0
|
||||
}
|
||||
|
||||
setup_error() {
|
||||
step="$1"
|
||||
status="error"
|
||||
finish
|
||||
echo "::error title=${SCRIPT}::test setup: ${step}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Reserve a container ID ---------------------------------------------------
|
||||
# A lock file per ID, not just "first free one", because the engine only creates
|
||||
# the container minutes later and two jobs would otherwise pick the same ID.
|
||||
# VMs take the range 100 above the containers' (DevScripts 9100-9149,
|
||||
# ProxmoxVE 9150-9199).
|
||||
mkdir -p /run/ci-jobs
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
source "$(dirname "$0")/pve-run-vm.sh"
|
||||
CTID_BASE=$((CTID_BASE + 100))
|
||||
vm_wait_turn
|
||||
fi
|
||||
exec 9>/run/ci-jobs/.lock
|
||||
flock 9
|
||||
# On Incus the ID is an instance name: var_ctid=ci-9000 names the container.
|
||||
for ((id = CTID_BASE; id < CTID_BASE + CTID_COUNT; id++)); do
|
||||
name=$id
|
||||
[[ "$PLATFORM" == "incus" ]] && name="ci-$id"
|
||||
[[ -e "/run/ci-jobs/$name" ]] && continue
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
incus info "$name" &>/dev/null && continue
|
||||
else
|
||||
pvesh get /cluster/nextid --vmid "$id" &>/dev/null || continue
|
||||
fi
|
||||
echo "$job" >"/run/ci-jobs/$name"
|
||||
ctid=$name
|
||||
break
|
||||
done
|
||||
flock -u 9
|
||||
exec 9>&-
|
||||
[[ -n "$ctid" ]] || setup_error "no free ${kind} ID in ${CTID_BASE}-$((CTID_BASE + CTID_COUNT - 1))"
|
||||
|
||||
# --- Fetch the approved commit and point it at ourselves ------------------------
|
||||
mkdir -p "$tree"
|
||||
curl -fsSL --retry 3 "https://codeload.github.com/${PR_REPO}/tar.gz/${PR_SHA}" |
|
||||
tar -xz -C "$tree" --strip-components=1 || setup_error "could not download ${PR_REPO}@${PR_SHA:0:7}"
|
||||
[[ -f "$tree/$SCRIPT" ]] || fail "${SCRIPT} does not exist at ${PR_SHA:0:7}"
|
||||
|
||||
upstream_re="https://raw\.githubusercontent\.com/${UPSTREAM//./\\.}/(refs/heads/)?main"
|
||||
grep -rlE "$upstream_re" "$tree" --include='*.sh' --include='*.func' |
|
||||
xargs -r sed -i -E "s#${upstream_re}#${base}#g"
|
||||
|
||||
if [[ "$kind" == "vm" ]]; then
|
||||
vm_build_and_check
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- Build ----------------------------------------------------------------------
|
||||
echo "Building ${SCRIPT} (${OS:-default OS}) on ${PLATFORM} as CT ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
|
||||
hostname_safe="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-50)"
|
||||
build_env=(
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOME=/root TERM=dumb LANG=C.UTF-8
|
||||
mode=default MODE=default PHS_SILENT=1 DIAGNOSTICS=no
|
||||
var_ctid="$ctid" var_hostname="$hostname_safe" var_brg="$BRIDGE"
|
||||
"dev_mode=timing,keep" COMMUNITY_SCRIPTS_URL="$base"
|
||||
)
|
||||
if [[ "$PLATFORM" == "incus" ]]; then
|
||||
build_env+=(var_container_storage=default)
|
||||
else
|
||||
build_env+=(var_container_storage=local var_template_storage=local)
|
||||
fi
|
||||
touch "$RAW_LOG.start"
|
||||
[[ -n "$OS" ]] && build_env+=(var_os="$OS")
|
||||
[[ -n "$VAR_ARM64" ]] && build_env+=(var_arm64=yes)
|
||||
# Both engines turn var_github_token into GITHUB_TOKEN inside the container.
|
||||
[[ -n "${GH_API_TOKEN:-}" ]] && build_env+=(var_github_token="$GH_API_TOKEN")
|
||||
|
||||
# --- Answers for the script's questions ------------------------------------------
|
||||
# Nobody types into CI, so anything the install asks has to be answered up front.
|
||||
# AGENTS.md rule 24: a script reads var_<x> first and prompts only when it is
|
||||
# unset (or uses prompt_input_required ... "var_x"). Every such variable gets a
|
||||
# CI value here. The names come from json/<app>.json app_vars (DevScripts) and
|
||||
# from the read/prompt_ lines of the install script (both repos). A bare `read`
|
||||
# that ignores its variable still fails, and the report says why.
|
||||
ENGINE_VARS=" var_ctid var_hostname var_os var_version var_cpu var_ram var_disk var_brg var_net var_container_storage var_template_storage var_unprivileged var_tags var_arm64 var_testurl var_verbose var_timezone var_http_proxy var_http_no_proxy var_appversion var_github_token "
|
||||
host_app=$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}")
|
||||
ci_value() {
|
||||
case "${1,,}" in
|
||||
*pass* | *secret* | *token* | *key*) head -c 32 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | cut -c1-20 ;;
|
||||
*email* | *mail*) echo "ci@example.test" ;;
|
||||
*url* | *endpoint*) echo "https://ci-${host_app}.example.test" ;;
|
||||
# One name per variable: var_opencloud_collabora_fqdn ->
|
||||
# ci-opencloud-collabora.example.test, so several of them cannot collide.
|
||||
*host* | *domain* | *fqdn*)
|
||||
local part="${1#var_}"
|
||||
part=$(sed -E 's/_?(fqdn|hostname|host|domain)$//' <<<"${part,,}" | tr '_' '-')
|
||||
echo "ci-${part:-$host_app}.example.test"
|
||||
;;
|
||||
*user*) echo "ciadmin" ;;
|
||||
*) echo "ci-test" ;;
|
||||
esac
|
||||
}
|
||||
declare -A answers=()
|
||||
json="$tree/json/${app}.json"
|
||||
if [[ -f "$json" ]]; then
|
||||
while IFS=$'\t' read -r name default; do
|
||||
answers[$name]="$default"
|
||||
done < <(jq -r '(if type == "array" then .[0] else . end).app_vars // [] | .[] |
|
||||
select(.name | test("^var_[a-z0-9_]{1,40}$")) | [.name, (.default // "" | tostring)] | @tsv' "$json" 2>/dev/null)
|
||||
fi
|
||||
# Names built at run time from a loop, e.g. OpenCloud:
|
||||
# servers=("opencloud" "collabora" "wopi")
|
||||
# for server in "${servers[@]}"; do fqdn_var="var_opencloud_${server}_fqdn"
|
||||
# Each var_...${x}... template is expanded with the words of `for x in ...`
|
||||
# (or of the array that loop walks). Nothing is executed, only read.
|
||||
expand_templates() {
|
||||
local tpl x pre suf words arr w
|
||||
while read -r tpl; do
|
||||
x=$(sed -E 's/^[^$]*\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?.*$/\1/' <<<"$tpl")
|
||||
pre="${tpl%%\$*}"
|
||||
suf=$(sed -E 's/^[^$]*\$\{?[A-Za-z_][A-Za-z0-9_]*\}?//' <<<"$tpl")
|
||||
words=$(grep -m1 -oE "for ${x} in [^;]*" "$1" | sed -E "s/^for ${x} in //")
|
||||
if [[ "$words" =~ \$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\} ]]; then
|
||||
arr="${BASH_REMATCH[1]}"
|
||||
words=$(grep -m1 -oE "^\s*${arr}=\([^)]*\)" "$1" | sed -E "s/^\s*${arr}=\(//; s/\)$//")
|
||||
fi
|
||||
for w in $words; do
|
||||
w="${w//[\"\']/}"
|
||||
[[ "${pre}${w}${suf}" =~ ^var_[a-z0-9_]{1,40}$ ]] && echo "${pre}${w}${suf}"
|
||||
done
|
||||
done < <(grep -oE 'var_[a-z0-9_]*\$\{?[A-Za-z_][A-Za-z0-9_]*\}?[a-z0-9_]*' "$1" | sort -u)
|
||||
}
|
||||
|
||||
# The engine names the install script after APP (lower case, no spaces), which
|
||||
# need not match the ct/ file name: ct/bluesky-pds.sh -> blueskypds-install.sh.
|
||||
nsapp=$(grep -m1 -oE '^APP="[^"]+"' "$tree/$SCRIPT" | cut -d'"' -f2 | tr -d ' ' | tr '[:upper:]' '[:lower:]')
|
||||
for f in "$tree/install/${app}-install.sh" "$tree/install/${nsapp}-install.sh" "$tree/$SCRIPT"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
while read -r name; do
|
||||
[[ -v "answers[$name]" ]] || answers[$name]=""
|
||||
done < <({
|
||||
grep -E '\bread\b|prompt_(input|input_required|confirm|select|password)\b' "$f" | grep -oE '\bvar_[a-z0-9_]{1,40}\b'
|
||||
expand_templates "$f"
|
||||
} | sort -u)
|
||||
done
|
||||
for name in "${!answers[@]}"; do
|
||||
[[ "$ENGINE_VARS" == *" $name "* ]] && continue
|
||||
value="${answers[$name]}"
|
||||
[[ -n "$value" ]] || value=$(ci_value "$name")
|
||||
build_env+=("$name=$value")
|
||||
case "${name,,}" in
|
||||
*pass* | *secret* | *token* | *key*) echo "Pre-answered ${name}=(random)" ;;
|
||||
*) echo "Pre-answered ${name}=${value}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# In the background so that a TERM from a cancelled job is handled at once:
|
||||
# bash defers traps until a foreground command returns.
|
||||
env -i "${build_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
|
||||
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 &
|
||||
build_pid=$!
|
||||
start_streams
|
||||
wait "$build_pid"
|
||||
exit_code=$?
|
||||
build_pid=""
|
||||
stop_streams
|
||||
|
||||
if ((exit_code == 124 || exit_code == 137)); then
|
||||
fail "build timed out after ${BUILD_TIMEOUT}"
|
||||
fi
|
||||
# The engine marks the step that broke with a cross. Its closing "Installation
|
||||
# failed in container" line is generic, so the cross before it is the useful one.
|
||||
failure_line() {
|
||||
local line
|
||||
line=$(strip_ansi <"$RAW_LOG" | grep -E '✖|\[ERROR\]|error:' | grep -v 'Installation failed in container' |
|
||||
tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-200)
|
||||
# A prompt nobody can answer: say what to do instead.
|
||||
# First, because the report keeps 200 characters.
|
||||
if [[ "$line" == *"read exited with status"* || "$line" == *"executing command read "* ]]; then
|
||||
local asked
|
||||
asked=$(grep -oE '\bvar_[a-z0-9_]{1,40}\b' <<<"$line" | tail -n 1)
|
||||
line="unanswerable prompt (AGENTS.md rule 24): ask only when ${asked:-var_<x>} is unset, or use prompt_input_required. ${line}"
|
||||
fi
|
||||
echo "$line"
|
||||
}
|
||||
if ((exit_code != 0)); then
|
||||
last=$(failure_line)
|
||||
step="build exited ${exit_code}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
# With dev_mode=keep (needed to read the install log) the engine carries on
|
||||
# after a failed install and still prints "Completed Successfully", exit 0.
|
||||
install_exit=$(strip_ansi <"$RAW_LOG" | grep -oE 'Installation failed in container [0-9]+ \(exit code: [0-9]+\)' |
|
||||
grep -oE '[0-9]+\)$' | tr -d ')' | tail -n 1)
|
||||
if [[ -n "$install_exit" ]]; then
|
||||
last=$(failure_line)
|
||||
step="install script exited ${install_exit}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
grep -qi 'completed successfully' "$RAW_LOG" || fail "build exited 0 but never reported success"
|
||||
|
||||
# --- Checks ---------------------------------------------------------------------
|
||||
guest_running || fail "container ${ctid} is not running"
|
||||
|
||||
ct_ip=$(guest_exec sh -c "ip -4 -o addr show dev eth0 | awk '{print \$4}' | cut -d/ -f1" 2>/dev/null | head -n1)
|
||||
|
||||
# What the app's page looks like, for the PR comment. Chromium runs as its own
|
||||
# unprivileged user with its sandbox on: the page is served by code under test.
|
||||
# ci-shot-firewall (pve-ci-host-setup.sh) keeps that user off this host and the
|
||||
# lab network; without it there is no screenshot rather than an unguarded one.
|
||||
# $1: file name in OUT_DIR (screenshot.png after install, -update after update).
|
||||
take_screenshot() {
|
||||
command -v chromium >/dev/null && id ci-shot &>/dev/null || return 0
|
||||
iptables -w -S CI_SHOT 2>/dev/null | grep -q -- '-d 10.0.0.0/8 .*REJECT' || return 0
|
||||
local dir
|
||||
dir=$(mktemp -d)
|
||||
chown ci-shot "$dir"
|
||||
# HOME: runuser keeps root's, which ci-shot cannot write; Playwright's
|
||||
# Chromium (the ARM runner) then crashes in its crash handler.
|
||||
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
|
||||
--ignore-certificate-errors --no-first-run --window-size=1280,800 --virtual-time-budget=10000 \
|
||||
--user-data-dir="$dir/profile" --screenshot="$dir/shot.png" "$url" &>/dev/null
|
||||
# Only a plausible PNG leaves this function.
|
||||
if [[ -s "$dir/shot.png" && $(stat -c %s "$dir/shot.png") -lt 3000000 ]] &&
|
||||
[[ "$(head -c 8 "$dir/shot.png" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
|
||||
cp "$dir/shot.png" "$OUT_DIR/${1:-screenshot.png}"
|
||||
fi
|
||||
rm -rf -- "$dir"
|
||||
}
|
||||
|
||||
# Is the app up? No failed services, and the URL the script tells the user to
|
||||
# open, e.g. http://${IP}:1337/admin, answers 200 (after redirects, so a jump
|
||||
# to /login is fine). A script that prints no URL has no web UI to check.
|
||||
# Run after the install and again after the update. Sets health_problem.
|
||||
pattern=$(grep -oE 'https?://\$\{IP\}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?' "$tree/$SCRIPT" | head -n1)
|
||||
[[ -n "$pattern" && -n "$ct_ip" ]] && url="${pattern/\$\{IP\}/$ct_ip}"
|
||||
health_problem=""
|
||||
check_health() {
|
||||
health_problem=""
|
||||
failed_units=""
|
||||
if guest_exec sh -c 'command -v systemctl' &>/dev/null; then
|
||||
sleep 10
|
||||
failed_units=$(guest_exec systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' | paste -sd, -)
|
||||
elif guest_exec sh -c 'command -v rc-status' &>/dev/null; then
|
||||
failed_units=$(guest_exec rc-status --crashed 2>/dev/null | awk '{print $1}' | paste -sd, -)
|
||||
fi
|
||||
[[ -z "$failed_units" ]] || { health_problem="failed services: ${failed_units}"; return 1; }
|
||||
[[ -n "$url" ]] || return 0
|
||||
local waited
|
||||
for ((waited = 0; waited < URL_WAIT; waited += 5)); do
|
||||
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
|
||||
sleep 5
|
||||
done
|
||||
# Also when the check fails: an error page explains more than a status code.
|
||||
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot "$1"
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] ||
|
||||
{ health_problem="${url} answered ${http_code/#000/nothing} instead of 200 within ${URL_WAIT}s"; return 1; }
|
||||
}
|
||||
|
||||
check_health screenshot.png || fail "$health_problem"
|
||||
|
||||
# --- Update -------------------------------------------------------------------------
|
||||
# A fresh install is already on the latest release, so a plain `update` would
|
||||
# only say "No update available" and never run update_script's real path. The
|
||||
# version files the install wrote (~/.<app>, one per fetch_and_deploy_gh_release)
|
||||
# that update_script checks are set back to 0.0.0-ci first; check_for_gh_release then sees an update and
|
||||
# runs it all. `update` is the /usr/bin/update the install generated: it pulls
|
||||
# ct/<app>.sh from the URL this container was built from, i.e. the PR's tree.
|
||||
UPDATE_FAKE="0.0.0-ci"
|
||||
update_fail() {
|
||||
update_status="fail"
|
||||
update_step="$1"
|
||||
fail "update: $1"
|
||||
}
|
||||
|
||||
version_files=$(guest_exec sh -c '
|
||||
for f in /root/.*; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in */.install-* | *.pending | *.bak | */.bash* | */.profile | */.hushlogin | */.viminfo | */.lesshst | */.wget-hsts | */.selected_editor) continue ;; esac
|
||||
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^v?[0-9]+(\.[0-9A-Za-z_+-]+)+$" "$f" && echo "$f"
|
||||
done' 2>/dev/null | grep -E '^/root/\.[A-Za-z0-9._-]+$')
|
||||
# Only set back what update_script checks: the names it passes to
|
||||
# check_for_*release. The install can also deploy helpers the update never
|
||||
# touches (immich: ~/.mise); those would stay 0.0.0-ci and fail the run. A
|
||||
# name that is a variable can't be read here, so then all of them are set back.
|
||||
tracked=$(grep -oE 'check_for_[a-z]*_?release[[:space:]]+("[^"]*"|[^[:space:]]+)' "$tree/$SCRIPT" |
|
||||
sed -E 's/^check_for_[a-z]*_?release[[:space:]]+//; s/^"(.*)"$/\1/' | tr -d ' ' | tr '[:upper:]' '[:lower:]' | sort -u)
|
||||
if [[ -n "$tracked" && "$tracked" != *'$'* ]]; then
|
||||
version_files=$(grep -xF -f <(sed 's|^|/root/.|' <<<"$tracked") <<<"$version_files" || true)
|
||||
fi
|
||||
for f in $version_files; do
|
||||
guest_exec sh -c "echo '$UPDATE_FAKE' > '$f'; rm -f '$f.pending'" ||
|
||||
update_fail "could not reset $f"
|
||||
done
|
||||
[[ -n "$version_files" ]] && echo "Update test: set back $(tr '\n' ' ' <<<"$version_files")to ${UPDATE_FAKE}"
|
||||
|
||||
: >"$UPDATE_RAW"
|
||||
update_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
||||
echo "──────── update test (live) ────────"
|
||||
echo "::stop-commands::${update_token}"
|
||||
# timeout runs a program, not a shell function, so spell out the exec here.
|
||||
if [[ "$PLATFORM" == "incus" ]]; then guest_cmd=(incus exec "$ctid" --); else guest_cmd=(pct exec "$ctid" --); fi
|
||||
timeout --kill-after=1m "$UPDATE_TIMEOUT" "${guest_cmd[@]}" env PHS_SILENT=1 mode=default MODE=default \
|
||||
TERM=dumb DIAGNOSTICS=no var_ignore_breaking_changes=yes ${GH_API_TOKEN:+GITHUB_TOKEN="$GH_API_TOKEN"} update </dev/null \
|
||||
> >(tee "$UPDATE_RAW" | live_filter) 2>&1 &
|
||||
update_pid=$!
|
||||
wait "$update_pid"
|
||||
update_rc=$?
|
||||
update_pid=""
|
||||
sleep 1
|
||||
echo "::${update_token}::"
|
||||
|
||||
update_failure_line() {
|
||||
strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]|error:' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180
|
||||
}
|
||||
if ((update_rc == 124 || update_rc == 137)); then
|
||||
update_fail "timed out after ${UPDATE_TIMEOUT}"
|
||||
elif ((update_rc != 0)); then
|
||||
last=$(update_failure_line)
|
||||
update_fail "exited ${update_rc}${last:+: $last}"
|
||||
fi
|
||||
# The engine can report an error and still exit 0 (bark-server: "github: rate
|
||||
# limit reached", then "Cleaned"). A rate limit is ours, not the PR's: the
|
||||
# update could not be tested. Any other error line is a failed update.
|
||||
update_error=$(strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
|
||||
if [[ -n "$update_error" ]]; then
|
||||
if [[ "${update_error,,}" == *"rate limit"* ]]; then
|
||||
update_status="error"
|
||||
update_step="could not be tested: ${update_error}"
|
||||
status="pass"
|
||||
finish
|
||||
exit 0
|
||||
fi
|
||||
update_fail "$update_error"
|
||||
fi
|
||||
if strip_ansi <"$UPDATE_RAW" | grep -qi 'updated successfully'; then
|
||||
update_status="updated"
|
||||
for f in $version_files; do
|
||||
[[ "$(guest_exec cat "$f" 2>/dev/null)" != "$UPDATE_FAKE" ]] ||
|
||||
update_fail "reported success, but $f is still ${UPDATE_FAKE}"
|
||||
done
|
||||
else
|
||||
# Ran cleanly without updating anything: no version file to fake (apt/npm
|
||||
# based), or an update_script that decides differently. Not a failure.
|
||||
update_status="no-update"
|
||||
update_step=$(strip_ansi <"$UPDATE_RAW" | grep -iE 'no update|up to date|up-to-date|already' | tail -n 1 | sed -E 's/^\s*(✔️?|ℹ️?|\s)*//' | cut -c1-160)
|
||||
fi
|
||||
|
||||
check_health screenshot-update.png || update_fail "after the update: $health_problem"
|
||||
|
||||
status="pass"
|
||||
finish
|
||||
Generated
-140
@@ -1,140 +0,0 @@
|
||||
# shellcheck shell=bash
|
||||
# The vm/ half of pve-run-script.sh, sourced from it: same inputs, same
|
||||
# result.json, same live output. Only building and checking differ.
|
||||
#
|
||||
# A VM script runs entirely on this host (there is no install script inside
|
||||
# the guest to follow), unattended through the core engine's VM_UNATTENDED=1:
|
||||
# the script's defaults, with ID, name, bridge and storage set from here. The
|
||||
# guest then boots on vmbr1 like the containers do, so its address is whatever
|
||||
# this host's dnsmasq leased to its MAC; no guest agent needed.
|
||||
#
|
||||
# What passes: the script exits 0 and says "Completed successfully", the VM is
|
||||
# running and keeps running through the boot wait, and where the script prints
|
||||
# a URL built from the VM's address (http://${VM_IP}:9000) that URL answers 200.
|
||||
# An installer ISO or an image without DHCP gets no address; that is reported,
|
||||
# not failed. There is no update test: a VM has no `update`.
|
||||
|
||||
VM_BOOT_WAIT="${VM_BOOT_WAIT:-300}"
|
||||
VM_URL_WAIT="${VM_URL_WAIT:-600}"
|
||||
LEASES="${LEASES:-/var/lib/misc/dnsmasq.leases}"
|
||||
|
||||
# One VM build per host at a time: a VM takes 2-8 GB of RAM and a disk image,
|
||||
# where a container takes a fraction. Held until the job exits; the build is
|
||||
# started with this descriptor closed, so a QEMU process can never inherit it.
|
||||
vm_wait_turn() {
|
||||
exec 7>/run/ci-jobs/.vm-turn
|
||||
flock -n 7 && return 0
|
||||
echo "Another VM build is running on this host; waiting for it (one VM at a time)"
|
||||
flock -w 3600 7 || setup_error "waited an hour for this host's VM slot"
|
||||
}
|
||||
|
||||
vm_cleanup() {
|
||||
if qm status "$ctid" &>/dev/null; then
|
||||
qm stop "$ctid" --skiplock 1 --timeout 30 &>/dev/null || true
|
||||
qm destroy "$ctid" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null || true
|
||||
fi
|
||||
rm -f "/run/ci-jobs/$ctid" "/run/ci-jobs/console-${ctid}.png"
|
||||
# Cloud-Init network snippets the script wrote for this VM.
|
||||
[[ -f "$RAW_LOG.start" ]] &&
|
||||
find /var/lib/vz/snippets -maxdepth 1 -type f -name "*${ctid}*" -newer "$RAW_LOG.start" -delete 2>/dev/null
|
||||
true
|
||||
}
|
||||
|
||||
# The VM's screen, straight from QEMU. Pixels only, but they come from the
|
||||
# guest, so the same checks as for a browser screenshot apply.
|
||||
vm_console_shot() {
|
||||
local f="/run/ci-jobs/console-${ctid}.png"
|
||||
rm -f "$f"
|
||||
printf 'screendump %s -f png\n' "$f" | timeout 30 qm monitor "$ctid" &>/dev/null
|
||||
if [[ -s "$f" && $(stat -c %s "$f") -lt 3000000 ]] &&
|
||||
[[ "$(head -c 8 "$f" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
|
||||
cp "$f" "$OUT_DIR/screenshot.png"
|
||||
fi
|
||||
rm -f "$f"
|
||||
}
|
||||
|
||||
vm_running() {
|
||||
qm status "$ctid" 2>/dev/null | grep -q 'status: running'
|
||||
}
|
||||
|
||||
vm_build_and_check() {
|
||||
local hn mac pattern waited last
|
||||
hn="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-40)-${ctid}"
|
||||
echo "Building ${SCRIPT} as VM ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
|
||||
local vm_env=(
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
HOME=/root TERM=dumb LANG=C.UTF-8 DIAGNOSTICS=no
|
||||
VM_UNATTENDED=1 VM_VMID="$ctid" VM_HOSTNAME="$hn" VM_BRIDGE="$BRIDGE"
|
||||
VM_STORAGE=local VM_ISO_STORAGE=local VM_START=yes
|
||||
COMMUNITY_SCRIPTS_URL="$base"
|
||||
)
|
||||
# Release lookups (vm_latest_from_github and the like).
|
||||
[[ -n "${GH_API_TOKEN:-}" ]] && vm_env+=(GITHUB_TOKEN="$GH_API_TOKEN")
|
||||
touch "$RAW_LOG.start"
|
||||
|
||||
env -i "${vm_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
|
||||
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 7>&- &
|
||||
build_pid=$!
|
||||
start_streams
|
||||
wait "$build_pid"
|
||||
exit_code=$?
|
||||
build_pid=""
|
||||
stop_streams
|
||||
|
||||
((exit_code == 124 || exit_code == 137)) && fail "build timed out after ${BUILD_TIMEOUT}"
|
||||
if ((exit_code != 0)); then
|
||||
last=$(failure_line)
|
||||
step="script exited ${exit_code}${last:+: $last}"
|
||||
status="fail"
|
||||
classify_cpu_failure
|
||||
fail "$step" "$status"
|
||||
fi
|
||||
# vm_finish ends on a msg_ok of the script's choosing ("VM created. ..."),
|
||||
# older scripts on "Completed successfully". exit_script and friends leave
|
||||
# with 0 too, but on a cross.
|
||||
last=$(strip_ansi <"$RAW_LOG" | tail -n 1 | cut -c1-200)
|
||||
if ! grep -qi 'completed successfully' "$RAW_LOG" && ! [[ "$last" =~ ^[[:space:]]*✔ ]]; then
|
||||
fail "script exited 0 but never reported success${last:+ (last line: ${last})}"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} is not running after the script finished"
|
||||
|
||||
# The address dnsmasq handed to this VM's MAC. The VM must stay up while it
|
||||
# boots: one that powers itself off or crashes is a failure.
|
||||
mac=$(qm config "$ctid" 2>/dev/null | sed -nE 's/^net0:[^=]*=([0-9A-Fa-f:]{17}).*/\1/p' | tr 'A-F' 'a-f')
|
||||
echo "Waiting up to ${VM_BOOT_WAIT}s for VM ${ctid} (${mac:-no NIC}) to boot and get an address"
|
||||
for ((waited = 0; waited < VM_BOOT_WAIT; waited += 5)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while booting"
|
||||
[[ -n "$mac" ]] && ct_ip=$(awk -v m="$mac" '$2 == m {print $3}' "$LEASES" 2>/dev/null | tail -n 1)
|
||||
[[ -n "$ct_ip" ]] && break
|
||||
sleep 5
|
||||
done
|
||||
[[ -n "$ct_ip" ]] && echo "VM ${ctid} got ${ct_ip}" || echo "VM ${ctid} got no DHCP lease within ${VM_BOOT_WAIT}s"
|
||||
|
||||
# Only a URL the script builds from the address it found; <VM-IP> in a
|
||||
# summary is a hint for later (after an installer ran), not a promise.
|
||||
# ${VM_IP}, $VM_IP and ${VM_IP:-<VM-IP>} all count.
|
||||
local ip_re='\$\{?VM_IP(:-[^}]*)?\}?'
|
||||
pattern=$(grep -oE "https?://${ip_re}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?" "$tree/$SCRIPT" | head -n1)
|
||||
if [[ -n "$pattern" ]]; then
|
||||
[[ -n "$ct_ip" ]] || fail "no IP address within ${VM_BOOT_WAIT}s, so ${pattern} could not be checked"
|
||||
url=$(sed -E "s#${ip_re}#${ct_ip}#" <<<"$pattern")
|
||||
for ((waited = 0; waited < VM_URL_WAIT; waited += 10)); do
|
||||
vm_running || fail "VM ${ctid} stopped by itself while its web UI was awaited"
|
||||
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
|
||||
sleep 10
|
||||
done
|
||||
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot screenshot.png
|
||||
else
|
||||
# Let the console settle on a login prompt rather than a boot message.
|
||||
sleep 20
|
||||
fi
|
||||
[[ -s "$OUT_DIR/screenshot.png" ]] || vm_console_shot
|
||||
if [[ -n "$url" && ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
|
||||
fail "${url} answered ${http_code/#000/nothing} instead of 200 within ${VM_URL_WAIT}s"
|
||||
fi
|
||||
vm_running || fail "VM ${ctid} stopped by itself"
|
||||
|
||||
status="pass"
|
||||
finish
|
||||
}
|
||||
Generated
+73
-95
@@ -22,112 +22,90 @@ jobs:
|
||||
const now = new Date();
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const TEMPLATE_LABEL = "missing pr template";
|
||||
const MAINTAINERS = ["OWNER", "MEMBER", "COLLABORATOR"];
|
||||
const days = d => (now - d) / (1000 * 60 * 60 * 24);
|
||||
const staleNotice = author =>
|
||||
`@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`;
|
||||
|
||||
// Only fires for a maintainer's label: labels set with GITHUB_TOKEN raise no event.
|
||||
|
||||
// --- When stale label is added, comment immediately ---
|
||||
if (context.eventName === "pull_request_target" && context.payload.action === "labeled") {
|
||||
const pr = context.payload.pull_request;
|
||||
const labels = (pr.labels || []).map(l => l.name);
|
||||
if (context.payload.label?.name === "stale" && !labels.includes(TEMPLATE_LABEL)) {
|
||||
const label = context.payload.label?.name;
|
||||
if (label === "stale") {
|
||||
const author = context.payload.pull_request.user.login;
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number, body: staleNotice(pr.user.login)
|
||||
owner,
|
||||
repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: `@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
|
||||
|
||||
|
||||
// --- Scheduled run: check all stale PRs ---
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner,
|
||||
repo,
|
||||
state: "open",
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
for (const pr of prs) {
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
const author = pr.user.login;
|
||||
const hasStale = labels.includes("stale");
|
||||
|
||||
if (labels.includes("keep-open")) {
|
||||
if (hasStale) {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: pr.number, per_page: 100
|
||||
const hasStale = pr.labels.some(l => l.name === "stale");
|
||||
if (!hasStale) continue;
|
||||
|
||||
// Get timeline events to find when stale label was added
|
||||
const { data: events } = await github.rest.issues.listEvents({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
per_page: 100
|
||||
});
|
||||
const commits = await github.paginate(github.rest.pulls.listCommits, {
|
||||
owner, repo, pull_number: pr.number, per_page: 100
|
||||
});
|
||||
const lastCommitDate = commits.length > 0
|
||||
? new Date(commits[commits.length - 1].commit.author.date)
|
||||
: new Date(pr.created_at);
|
||||
const humanComments = comments.filter(c => c.user?.type !== "Bot");
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Auto-label PRs with no activity in the last 14 days,
|
||||
// but ONLY if someone other than the author has engaged.
|
||||
// -------------------------------------------------------
|
||||
if (!hasStale) {
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, {
|
||||
owner, repo, pull_number: pr.number, per_page: 100
|
||||
});
|
||||
const engaged =
|
||||
reviews.some(r => r.user?.type !== "Bot" && r.user?.login !== author) ||
|
||||
humanComments.some(c => c.user?.login !== author);
|
||||
if (!engaged) continue;
|
||||
|
||||
const lastCommentDate = humanComments.length > 0
|
||||
? new Date(humanComments[humanComments.length - 1].created_at)
|
||||
: null;
|
||||
const lastActivityDate = lastCommentDate && lastCommentDate > lastCommitDate
|
||||
? lastCommentDate
|
||||
: lastCommitDate;
|
||||
|
||||
if (days(lastActivityDate) > 14) {
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ["stale"] });
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: staleNotice(author) });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Manage already-stale PRs
|
||||
// -------------------------------------------------------
|
||||
const events = await github.paginate(github.rest.issues.listEvents, {
|
||||
owner, repo, issue_number: pr.number, per_page: 100
|
||||
});
|
||||
const staleDates = events
|
||||
|
||||
// Find the most recent time the stale label was added
|
||||
const staleLabelEvents = events
|
||||
.filter(e => e.event === "labeled" && e.label?.name === "stale")
|
||||
.map(e => new Date(e.created_at));
|
||||
if (staleDates.length === 0) continue;
|
||||
const staleLabelDate = new Date(Math.max(...staleDates));
|
||||
const isTemplateIssue = labels.includes(TEMPLATE_LABEL);
|
||||
|
||||
// A push ends an inactivity mark; a template mark only ends via close-invalid-pr-template.yml's re-check.
|
||||
if (!isTemplateIssue && lastCommitDate > staleLabelDate) {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
|
||||
if (staleLabelEvents.length === 0) continue;
|
||||
|
||||
const staleLabelDate = new Date(staleLabelEvents[0].created_at);
|
||||
const daysSinceStale = (now - staleLabelDate) / (1000 * 60 * 60 * 24);
|
||||
|
||||
// Check for new commits since stale label was added
|
||||
const { data: commits } = await github.rest.pulls.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
|
||||
const lastCommitDate = new Date(commits[commits.length - 1].commit.author.date);
|
||||
const author = pr.user.login;
|
||||
|
||||
// If there are new commits after the stale label, remove it
|
||||
if (lastCommitDate > staleLabelDate) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
name: "stale"
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number,
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: `@${author} Recent activity detected. Removing stale label.`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// A maintainer comment after the mark restarts the 7 days.
|
||||
const maintainerDates = humanComments
|
||||
.filter(c => MAINTAINERS.includes(c.author_association) && c.user?.login !== author)
|
||||
.map(c => new Date(c.created_at))
|
||||
.filter(d => d > staleLabelDate);
|
||||
const clockStart = maintainerDates.length > 0 ? new Date(Math.max(...maintainerDates)) : staleLabelDate;
|
||||
if (days(clockStart) <= 7) continue;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number: pr.number,
|
||||
body: isTemplateIssue
|
||||
? `@${author} Closing: the PR template requirements listed above were still missing 7 days later. Once they are in place, please open a new pull request.`
|
||||
: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
|
||||
});
|
||||
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: "closed" });
|
||||
// If 7 days have passed since stale label, close the PR
|
||||
else if (daysSinceStale > 7) {
|
||||
await github.rest.pulls.update({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
state: "closed"
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr.number,
|
||||
body: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
|
||||
});
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -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 (ct scripts here load its PVE
|
||||
# backend; Incus needs the Incus one), so a plain 1:1 copy would break every Incus
|
||||
# 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
|
||||
# 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).
|
||||
#
|
||||
|
||||
+643
-805
File diff suppressed because it is too large.
Load diff
+7
-6
@@ -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 [DevScripts](https://github.com/community-scripts/DevScripts) first — not to this repository.
|
||||
> **New scripts** must always be submitted to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) 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** | [DevScripts](https://github.com/community-scripts/DevScripts) — testing repo for new scripts |
|
||||
| **Add a brand-new script** | [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) — 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 [DevScripts](https://github.com/community-scripts/DevScripts) and clone it
|
||||
1. Fork [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) 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 DevScripts — run the script against a real Proxmox instance
|
||||
5. Open a PR in **DevScripts** for review and testing
|
||||
4. Test thoroughly in ProxmoxVED — run the script against a real Proxmox instance
|
||||
5. Open a PR in **ProxmoxVED** 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,6 +191,7 @@ 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 [DevScripts](https://github.com/community-scripts/DevScripts) instead.
|
||||
- PRs with **new scripts** opened against ProxmoxVE will be closed — submit them to [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED) instead.
|
||||
- PRs that fail CI checks will not be merged.
|
||||
@@ -1,17 +1,16 @@
|
||||
<div align="center">
|
||||
<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. -->
|
||||
<img src="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/images/logo-81x112.png" height="112px" alt="Proxmox VE Helper-Scripts Logo" />
|
||||
|
||||
<h1>Community Scripts — Proxmox VE</h1>
|
||||
<p><strong>One-command installations for services, containers and VMs on Proxmox VE</strong><br/>
|
||||
<h1>Proxmox VE Helper-Scripts</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" 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>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -19,146 +18,94 @@
|
||||
|
||||
## What is this?
|
||||
|
||||
**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.
|
||||
**Simplify your Proxmox VE setup with community-driven automation scripts.**
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
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.
|
||||
The collection covers hundreds of services across categories like home automation, media servers, networking tools, databases, monitoring stacks, and more.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
| 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.
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Repository map
|
||||
## Getting Started
|
||||
|
||||
| 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 |
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Layout
|
||||
## How Scripts Work
|
||||
|
||||
| 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 |
|
||||
Every script follows the same pattern:
|
||||
|
||||
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.
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
## Testing a fork or branch
|
||||
## What's Included
|
||||
|
||||
Engine and scripts resolve independently, so either can point somewhere else.
|
||||
Put the variable on the command itself — nothing needs exporting.
|
||||
The repository covers a wide range of categories. A few examples:
|
||||
|
||||
To run your own copy of the scripts:
|
||||
| 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 |
|
||||
|
||||
```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`.
|
||||
> Browse the full list at **[community-scripts.org/categories](https://community-scripts.org/categories)** — new scripts are added regularly.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
This project runs on community contributions. New scripts, fixes, bug reports —
|
||||
all of it helps.
|
||||
This project runs on community contributions. Whether you want to write new scripts, improve existing ones, or just report a bug — every bit helps.
|
||||
|
||||
| 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) |
|
||||
### 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) |
|
||||
|
||||
### Before you open a PR
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -190,8 +137,6 @@ all of it helps.
|
||||
<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/>
|
||||
@@ -210,7 +155,6 @@ all of it helps.
|
||||
<sub><b>asylumexp</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -250,11 +194,9 @@ all of it helps.
|
||||
|
||||
## Support the Project
|
||||
|
||||
This project is maintained by volunteers. Infrastructure costs come out of
|
||||
pocket and the work happens in people's spare time.
|
||||
This project is maintained by volunteers. All infrastructure costs come out of pocket, and the work is done 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">
|
||||
@@ -270,13 +212,14 @@ care** — a cause that was important to tteck.
|
||||
|
||||
## License
|
||||
|
||||
MIT — free to use, modify and redistribute, for personal and commercial
|
||||
purposes. See [LICENSE](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).
|
||||
|
||||
---
|
||||
|
||||
<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>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>
|
||||
<sub><i>Proxmox® is a registered trademark of <a href="https://www.proxmox.com/en/about/company">Proxmox Server Solutions GmbH</a></i></sub>
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
+4
-2
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
@@ -37,9 +38,9 @@ function update_script() {
|
||||
msg_ok "Services Stopped"
|
||||
|
||||
create_backup /opt/adventurelog/backend/server/.env \
|
||||
/opt/adventurelog/frontend/.env
|
||||
/opt/adventurelog/backend/server/media
|
||||
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="backend/server/media" fetch_and_deploy_gh_release "adventurelog" "seanmorley15/adventurelog" "tarball"
|
||||
fetch_and_deploy_gh_release "adventurelog" "seanmorley15/adventurelog" "tarball"
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
msg_info "Ensuring PostgreSQL Extensions"
|
||||
@@ -56,6 +57,7 @@ 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
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -36,9 +37,9 @@ function update_script() {
|
||||
systemctl stop airtrail
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/airtrail/.env
|
||||
create_backup /opt/airtrail/.env /opt/airtrail/uploads
|
||||
|
||||
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="uploads" fetch_and_deploy_gh_release "airtrail" "johanohly/AirTrail" "tarball" "latest" "/opt/airtrail"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "airtrail" "johanohly/AirTrail" "tarball" "latest" "/opt/airtrail"
|
||||
|
||||
restore_backup
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
|
||||
@@ -48,7 +49,9 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
echo -e "${INFO}${YW}Please paste your SSH public key (e.g., ssh-rsa AAAAB3... user@host):${CL}"
|
||||
msg_info "Setting up SSH Public Key for backup user"
|
||||
|
||||
msg_info "Please paste your SSH public key (e.g., ssh-rsa AAAAB3... user@host): \n"
|
||||
read -p "Key: " SSH_PUBLIC_KEY
|
||||
echo
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+14
-6
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -29,13 +30,20 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ -f /opt/${APP}_version.txt ]]; then
|
||||
mv /opt/"${APP}_version.txt" ~/.it-tools
|
||||
fi
|
||||
|
||||
if check_for_gh_release "it-tools" "sharevb/it-tools"; then
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "it-tools" "sharevb/it-tools" "prebuild" "latest" "/usr/share/nginx/html" "it-tools-*.zip"
|
||||
RELEASE=$(curl -fsSL https://api.github.com/repos/sharevb/it-tools/releases/latest | grep '"tag_name":' | cut -d '"' -f4)
|
||||
if [ "${RELEASE}" != "$(cat /opt/${APP}_version.txt)" ] || [ ! -f /opt/${APP}_version.txt ]; then
|
||||
msg_info "Updating ${APP} LXC"
|
||||
curl -fsSL "https://github.com/sharevb/it-tools/releases/download/${RELEASE}/it-tools-${RELEASE#v}.zip" -o it-tools.zip
|
||||
mkdir -p /usr/share/nginx/html
|
||||
rm -rf /usr/share/nginx/html/*
|
||||
$STD unzip it-tools.zip -d /tmp
|
||||
cp -r /tmp/dist/* /usr/share/nginx/html
|
||||
rm -rf /tmp/dist
|
||||
rm -f it-tools.zip
|
||||
echo "${RELEASE}" >/opt/${APP}_version.txt
|
||||
msg_ok "Updated successfully!"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at ${RELEASE}"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -31,7 +32,7 @@ function update_script() {
|
||||
|
||||
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}"
|
||||
msg_info "This is a one-time migration. After this, you can update ${APP} anytime with:"
|
||||
echo -e "${TAB}${TAB}${GN}update_komodo${CL} or ${GN}bash <(curl -fsSL ${ADDON_SCRIPT})${CL}"
|
||||
echo ""
|
||||
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
|
||||
@@ -62,7 +63,7 @@ MIGRATION_EOF
|
||||
ln -sf /usr/bin/update /usr/bin/update_komodo 2>/dev/null || true
|
||||
msg_ok "Migration complete"
|
||||
|
||||
echo -e "${INFO}${YW}Running addon update${CL}"
|
||||
msg_info "Running addon update"
|
||||
type=update bash <(curl -fsSL "${ADDON_SCRIPT}")
|
||||
exit
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+17
-3
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
|
||||
@@ -29,7 +30,8 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "wakapi" "muety/wakapi"; then
|
||||
RELEASE=$(curl -s https://api.github.com/repos/muety/wakapi/releases/latest | grep "tag_name" | awk '{print substr($2, 2, length($2)-3) }')
|
||||
if [ "${RELEASE}" != "$(cat ~/.wakapi 2>/dev/null)" ] || [ ! -f ~/.wakapi ]; then
|
||||
msg_info "Stopping Wakapi Service"
|
||||
$STD rc-service wakapi stop
|
||||
msg_ok "Stopped Wakapi Service"
|
||||
@@ -38,14 +40,26 @@ function update_script() {
|
||||
$STD apk -U upgrade
|
||||
msg_ok "Updated Wakapi LXC"
|
||||
|
||||
create_backup /opt/wakapi/config.yml /opt/wakapi/wakapi_db.db
|
||||
msg_info "Creating backup"
|
||||
mkdir -p /opt/wakapi-backup
|
||||
cp /opt/wakapi/config.yml /opt/wakapi/wakapi_db.db /opt/wakapi-backup/
|
||||
msg_ok "Created backup"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "wakapi" "muety/wakapi" "prebuild" "latest" "/opt/wakapi" "wakapi_linux_$(arch_resolve).zip"
|
||||
restore_backup
|
||||
|
||||
msg_info "Configuring Wakapi"
|
||||
cd /opt/wakapi
|
||||
cp /opt/wakapi-backup/config.yml /opt/wakapi/
|
||||
cp /opt/wakapi-backup/wakapi_db.db /opt/wakapi/
|
||||
rm -rf /opt/wakapi-backup
|
||||
msg_ok "Configured Wakapi"
|
||||
|
||||
msg_info "Starting Service"
|
||||
$STD rc-service wakapi start
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at ${RELEASE}"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
@@ -1,41 +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://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!"
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+1
-1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -39,7 +40,6 @@ 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
|
||||
|
||||
@@ -1,65 +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://docs.ankiweb.net/sync-server.html
|
||||
|
||||
APP="Anki-Sync-Server"
|
||||
var_tags="${var_tags:-anki;flashcards;sync}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
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 [[ ! -f /opt/anki-sync-server/.env ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "anki-sync-server" "ankitects/anki"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop anki-sync-server
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/anki-sync-server/.env /opt/anki-sync-server/data
|
||||
|
||||
msg_info "Updating Anki Sync Server"
|
||||
$STD uv pip install --python /opt/anki-sync-server/venv/bin/python "anki==$(get_latest_github_release "ankitects/anki")"
|
||||
cat <<EOF >~/.anki-sync-server
|
||||
$(get_latest_github_release "ankitects/anki")
|
||||
EOF
|
||||
msg_ok "Updated Anki Sync Server"
|
||||
|
||||
restore_backup
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start anki-sync-server
|
||||
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}"
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/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}"
|
||||
+2
-13
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
|
||||
@@ -37,25 +38,13 @@ function update_script() {
|
||||
systemctl stop anytype
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/anytype/data /opt/anytype/.env
|
||||
create_backup /opt/anytype/data
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
@@ -31,7 +32,7 @@ function update_script() {
|
||||
fi
|
||||
msg_info "Updating Apache CouchDB"
|
||||
apt_update_safe
|
||||
$STD apt install -y --only-upgrade couchdb
|
||||
$STD apt-get install -y --only-upgrade couchdb
|
||||
msg_ok "Updated Apache CouchDB"
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+23
-19
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -51,28 +52,18 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
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[@]}"
|
||||
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}"
|
||||
|
||||
msg_info "Installing update"
|
||||
rm -rf "${TOMCAT_DIR:?}"/*
|
||||
@@ -80,8 +71,21 @@ function update_script() {
|
||||
rm -f /tmp/tomcat-update.tar.gz
|
||||
msg_ok "Installed update"
|
||||
|
||||
restore_backup
|
||||
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"
|
||||
chown -R root:root "$TOMCAT_DIR"
|
||||
msg_ok "Restored configuration and applications"
|
||||
|
||||
msg_info "Starting Tomcat service"
|
||||
systemctl start tomcat
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -46,7 +47,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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
+62
-12
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
@@ -12,7 +13,7 @@ var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-12}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
@@ -30,26 +31,75 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="@postlight/parser@latest,single-file-cli@latest" setup_nodejs
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
ensure_dependencies chromium
|
||||
setup_uv
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop archivebox
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Upgrading Playwright"
|
||||
$STD uv pip install playwright --system --break-system-packages
|
||||
$STD playwright install-deps chromium
|
||||
msg_ok "Upgraded Playwright"
|
||||
local migrate=0 py port base runtime
|
||||
if [[ ! -x /opt/archivebox/venv/bin/archivebox ]]; then
|
||||
migrate=1
|
||||
msg_info "Moving ArchiveBox to its own Python 3.13 environment"
|
||||
py="/usr/bin/python3.13"
|
||||
[[ -x "$py" ]] || ensure_dependencies python3.13 || true
|
||||
if [[ ! -x "$py" ]]; then
|
||||
$STD uv python install --install-dir /opt/archivebox/python 3.13
|
||||
py="$(UV_PYTHON_INSTALL_DIR=/opt/archivebox/python uv python find 3.13)"
|
||||
fi
|
||||
$STD uv venv --python "$py" /opt/archivebox/venv
|
||||
msg_ok "Moved ArchiveBox to its own Python 3.13 environment"
|
||||
fi
|
||||
|
||||
port="$(awk -F: '/^ExecStart=/ {print $NF}' /etc/systemd/system/archivebox.service 2>/dev/null)"
|
||||
[[ "$port" =~ ^[0-9]+$ ]] || port=5797
|
||||
cat <<EOF >/etc/systemd/system/archivebox.service
|
||||
[Unit]
|
||||
Description=ArchiveBox Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=archivebox
|
||||
WorkingDirectory=/opt/archivebox/data
|
||||
ExecStartPre=/opt/archivebox/venv/bin/archivebox init
|
||||
ExecStart=/opt/archivebox/venv/bin/archivebox server 0.0.0.0:${port}
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
|
||||
msg_info "Updating ArchiveBox"
|
||||
$STD uv pip install --python /opt/archivebox/venv/bin/python --upgrade archivebox
|
||||
ln -sf /opt/archivebox/venv/bin/archivebox /usr/local/bin/archivebox
|
||||
mkdir -p /home/archivebox/.config
|
||||
chown -R archivebox:archivebox /opt/archivebox /home/archivebox
|
||||
cd /opt/archivebox/data
|
||||
$STD uv pip install --system --break-system-packages --upgrade --no-reinstall archivebox
|
||||
sudo -u archivebox archivebox init
|
||||
$STD sudo -u archivebox /opt/archivebox/venv/bin/archivebox init
|
||||
base="$(grep -oP '^BASE_URL\s*=\s*\K\S+' ArchiveBox.conf 2>/dev/null || true)"
|
||||
if [[ -z "$base" || "$base" =~ ^https?://[0-9.]+(:[0-9]+)?/?$ ]]; then
|
||||
base="http://$(hostname -I | awk '{print $1}'):${port}"
|
||||
fi
|
||||
$STD sudo -u archivebox /opt/archivebox/venv/bin/archivebox config --set "BASE_URL=${base}" MERCURY_ENABLED=False
|
||||
msg_ok "Updated ArchiveBox"
|
||||
|
||||
msg_info "Installing Extractors (Patience)"
|
||||
runtime="/tmp/archivebox-runtime-$(id -u archivebox)"
|
||||
install -d -m 700 -o archivebox -g archivebox "$runtime"
|
||||
if $STD env HOME=/home/archivebox USER=archivebox LOGNAME=archivebox XDG_CONFIG_HOME=/home/archivebox/.config XDG_RUNTIME_DIR="$runtime" \
|
||||
/opt/archivebox/venv/bin/archivebox install; then
|
||||
msg_ok "Installed Extractors"
|
||||
else
|
||||
msg_warn "Some extractors failed to install - crawls stop until the next update succeeds"
|
||||
fi
|
||||
|
||||
if [[ "$migrate" == 1 ]]; then
|
||||
msg_info "Migrating Existing Snapshots (Patience)"
|
||||
$STD sudo -u archivebox /opt/archivebox/venv/bin/archivebox update --rescan --migrate-only --index-only
|
||||
msg_ok "Migrated Existing Snapshots"
|
||||
fi
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start archivebox
|
||||
msg_ok "Started Service"
|
||||
@@ -64,4 +114,4 @@ description
|
||||
msg_ok "Completed successfully!\n"
|
||||
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:8000/admin/login${CL}"
|
||||
echo -e "${GATEWAY}${BGN}http://${IP}:5797/admin/${CL}"
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
+2
-3
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -40,7 +41,6 @@ function update_script() {
|
||||
|
||||
AUDIOMUSE_BACKEND="$(cat /opt/audiomuse-ai_data/.backend 2>/dev/null || echo cpu)"
|
||||
REQ_COMMON="common.txt"
|
||||
PY_VERSION="3.12"
|
||||
case "$AUDIOMUSE_BACKEND" in
|
||||
gpu)
|
||||
REQ_ACCEL="gpu.txt"
|
||||
@@ -49,7 +49,6 @@ function update_script() {
|
||||
cpu-noavx2)
|
||||
REQ_COMMON="common-noavx2.txt"
|
||||
REQ_ACCEL="cpu-noavx2.txt"
|
||||
PY_VERSION="3.11"
|
||||
;;
|
||||
*)
|
||||
REQ_ACCEL="cpu.txt"
|
||||
@@ -58,7 +57,7 @@ function update_script() {
|
||||
|
||||
msg_info "Updating Python Environment (${AUDIOMUSE_BACKEND})"
|
||||
cd /opt/audiomuse-ai
|
||||
$STD uv venv --clear --seed --python "$PY_VERSION" /opt/audiomuse-ai/.venv
|
||||
$STD uv venv --seed --python 3.12 /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}"
|
||||
|
||||
+5
-6
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -37,16 +38,16 @@ function update_script() {
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "aurral" "lklynet/aurral" "tarball"
|
||||
NODE_VERSION="26" NODE_MODULE="pnpm" setup_nodejs
|
||||
ensure_dependencies git
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
|
||||
msg_info "Updating Aurral"
|
||||
cd /opt/aurral
|
||||
export VITE_APP_VERSION="$(cat ~/.aurral)"
|
||||
export VITE_GITHUB_REPO="lklynet/aurral"
|
||||
export VITE_RELEASE_CHANNEL="stable"
|
||||
$STD pnpm install --frozen-lockfile --config.engine-strict=false --filter aurral-frontend --filter aurral-backend
|
||||
$STD pnpm --filter aurral-frontend run build
|
||||
$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
|
||||
mkdir -p /opt/aurral_data
|
||||
cat <<EOF >/opt/aurral/aurral.env
|
||||
NODE_ENV=production
|
||||
@@ -54,8 +55,6 @@ 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"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -47,7 +48,7 @@ function update_script() {
|
||||
RUST_PROFILE="minimal" RUST_TOOLCHAIN="stable" setup_rust
|
||||
setup_yq
|
||||
|
||||
AUTHENTIK_VERSION="version/2026.8.5"
|
||||
AUTHENTIK_VERSION="version/2026.8.3"
|
||||
# Source: https://github.com/goauthentik/fips/blob/main/Makefile#L26
|
||||
XMLSEC_VERSION="1.3.12"
|
||||
|
||||
@@ -136,6 +137,7 @@ function update_script() {
|
||||
export UV_COMPILE_BYTECODE="1"
|
||||
export UV_LINK_MODE="copy"
|
||||
export UV_NATIVE_TLS="1"
|
||||
export UV_HTTP_TIMEOUT="300"
|
||||
export RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
export UV_PYTHON_INSTALL_DIR="/usr/local/bin"
|
||||
cd /opt/authentik
|
||||
@@ -155,8 +157,7 @@ function update_script() {
|
||||
rm -r /opt/authentik/blueprints
|
||||
$STD find /opt/authentik-data -path '*/lost+found' -prune -o -exec chown authentik:authentik {} +
|
||||
|
||||
# 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
|
||||
if [[ $MAJOR == 2026 && $MINOR -lt 8 ]]; then
|
||||
msg_info "Updating Worker and Server config (from $MAJOR.$MINOR)"
|
||||
cat <<EOF >>/etc/default/authentik-server
|
||||
RUST_BACKTRACE=full
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -58,7 +59,7 @@ function update_script() {
|
||||
mkdir -p "$INSTALL_DIR"/metadata_temp
|
||||
$STD tar -xf ~/autocaliweb_bkp.tar --directory /
|
||||
KEPUB_VERSION="$(/usr/bin/kepubify --version)"
|
||||
CALIBRE_RELEASE="$(get_latest_github_release "kovidgoyal/calibre")"
|
||||
CALIBRE_RELEASE="$(curl -s https://api.github.com/repos/kovidgoyal/calibre/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)"
|
||||
echo "${KEPUB_VERSION#v}" >"$INSTALL_DIR"/KEPUBIFY_RELEASE
|
||||
echo "${CALIBRE_RELEASE#v}" >/"$INSTALL_DIR"/CALIBRE_RELEASE
|
||||
sed 's/^/v/' ~/.autocaliweb >"$INSTALL_DIR"/ACW_RELEASE
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+1
-8
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -46,19 +47,11 @@ 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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -33,7 +34,7 @@ function update_script() {
|
||||
if check_for_gh_release "beszel" "henrygd/beszel"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop beszel-hub
|
||||
msg_ok "Stopped Service"
|
||||
msg_info "Stopped Service"
|
||||
|
||||
msg_info "Updating Beszel"
|
||||
$STD /opt/beszel/beszel update
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+20
-18
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -75,7 +76,8 @@ update_deb_based() {
|
||||
|
||||
msg_info "Configuring Bitmagnet"
|
||||
cd /opt/bitmagnet
|
||||
$STD go build -ldflags "-s -w -X github.com/bitmagnet-io/bitmagnet/internal/version.GitTag=v$(cat ~/.bitmagnet)"
|
||||
VREL=v$(curl -fsSL https://api.github.com/repos/bitmagnet-io/bitmagnet/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
||||
$STD go build -ldflags "-s -w -X github.com/bitmagnet-io/bitmagnet/internal/version.GitTag=$VREL"
|
||||
chmod +x bitmagnet
|
||||
msg_ok "Configured Bitmagnet"
|
||||
|
||||
@@ -91,10 +93,8 @@ update_alpine() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if [[ -f /opt/bitmagnet_version.txt ]]; then
|
||||
mv /opt/bitmagnet_version.txt ~/.bitmagnet
|
||||
fi
|
||||
if check_for_gh_release "bitmagnet" "bitmagnet-io/bitmagnet"; then
|
||||
RELEASE=$(curl -fsSL https://api.github.com/repos/bitmagnet-io/bitmagnet/releases/latest | grep "tag_name" | awk '{print substr($2, 3, length($2)-4) }')
|
||||
if [ "${RELEASE}" != "$(cat /opt/bitmagnet_version.txt)" ] || [ ! -f /opt/bitmagnet_version.txt ]; then
|
||||
msg_info "Backing up database"
|
||||
rm -f /tmp/backup.sql
|
||||
$STD sudo -u postgres pg_dump \
|
||||
@@ -120,25 +120,27 @@ update_alpine() {
|
||||
mv /tmp/backup.sql /opt/
|
||||
msg_ok "Database backed up"
|
||||
|
||||
msg_info "Stopping Service"
|
||||
msg_info "Updating ${APP} from $(cat /opt/bitmagnet_version.txt) to ${RELEASE}"
|
||||
$STD apk -U upgrade
|
||||
$STD service bitmagnet stop
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/bitmagnet/.env /opt/bitmagnet/config.yml
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bitmagnet" "bitmagnet-io/bitmagnet" "tarball"
|
||||
restore_backup
|
||||
|
||||
msg_info "Building ${APP}"
|
||||
[ -f /opt/bitmagnet/.env ] && cp /opt/bitmagnet/.env /opt/
|
||||
[ -f /opt/bitmagnet/config.yml ] && cp /opt/bitmagnet/config.yml /opt/
|
||||
rm -rf /opt/bitmagnet/*
|
||||
temp_file=$(mktemp)
|
||||
curl -fsSL "https://github.com/bitmagnet-io/bitmagnet/archive/refs/tags/v${RELEASE}.tar.gz" -o "$temp_file"
|
||||
tar zxf "$temp_file" --strip-components=1 -C /opt/bitmagnet
|
||||
cd /opt/bitmagnet
|
||||
$STD go build -ldflags "-s -w -X github.com/bitmagnet-io/bitmagnet/internal/version.GitTag=v$(cat ~/.bitmagnet)"
|
||||
VREL=v$RELEASE
|
||||
$STD go build -ldflags "-s -w -X github.com/bitmagnet-io/bitmagnet/internal/version.GitTag=$VREL"
|
||||
chmod +x bitmagnet
|
||||
msg_ok "Built ${APP}"
|
||||
|
||||
msg_info "Starting Service"
|
||||
[ -f "/opt/.env" ] && cp "/opt/.env" /opt/bitmagnet/
|
||||
[ -f "/opt/config.yml" ] && cp "/opt/config.yml" /opt/bitmagnet/
|
||||
rm -f "$temp_file"
|
||||
echo "${RELEASE}" >/opt/bitmagnet_version.txt
|
||||
$STD service bitmagnet start
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
else
|
||||
msg_ok "No update required. ${APP} is already at ${RELEASE}"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/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}"
|
||||
+9
-45
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
@@ -9,30 +10,22 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
|
||||
APP="Blocky"
|
||||
var_tags="${var_tags:-adblock}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
if [[ -z "${var_os:-}" ]] && command -v pveversion >/dev/null 2>&1; then
|
||||
var_os=$(msg_menu "Choose the container OS" \
|
||||
"debian" "Debian 13" \
|
||||
"alpine" "Alpine 3.24 (smaller footprint)")
|
||||
fi
|
||||
|
||||
if [[ "${var_os:-}" == "alpine" ]]; then
|
||||
var_ram="${var_ram:-256}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_version="${var_version:-3.24}"
|
||||
else
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_version="${var_version:-13}"
|
||||
fi
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
update_deb_based() {
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /opt/blocky ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
@@ -54,35 +47,6 @@ update_deb_based() {
|
||||
exit
|
||||
}
|
||||
|
||||
update_alpine() {
|
||||
if [[ ! -d /opt/blocky ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if check_for_gh_release "blocky" "0xERR0R/blocky"; then
|
||||
msg_info "Stopping Service"
|
||||
rc-service blocky stop
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
create_backup /opt/blocky/config.yml
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "blocky" "0xERR0R/blocky" "prebuild" "latest" "/opt/blocky" "blocky_*_Linux_$(arch_resolve "x86_64" "arm64").tar.gz"
|
||||
restore_backup
|
||||
|
||||
msg_info "Starting Service"
|
||||
rc-service blocky start
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
exit
|
||||
}
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
run_os_update
|
||||
}
|
||||
|
||||
start
|
||||
build_container
|
||||
description
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+2
-6
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -48,12 +49,7 @@ function update_script() {
|
||||
|
||||
$STD corepack prepare "pnpm@${PNPM_VERSION}" --activate
|
||||
$STD pnpm install --frozen-lockfile
|
||||
# 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 client run build-only
|
||||
$STD pnpm --filter server run build
|
||||
cp -r /opt/bookorbit/client/dist /opt/bookorbit/server/public
|
||||
mkdir -p /opt/bookorbit/server/migrations
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
+12
-33
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
@@ -31,9 +32,15 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
local changed=0
|
||||
if grep -q -- '--workers 2' /etc/systemd/system/borg-ui.service 2>/dev/null; then
|
||||
msg_info "Reducing Service to a single worker"
|
||||
sed -i 's/--workers 2/--workers 1/' /etc/systemd/system/borg-ui.service
|
||||
systemctl daemon-reload
|
||||
systemctl try-restart borg-ui
|
||||
msg_ok "Reduced Service to a single worker"
|
||||
fi
|
||||
|
||||
if check_for_gh_release "borg-ui" "karanhudia/borg-ui"; then
|
||||
changed=1
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop borg-ui
|
||||
msg_ok "Stopped Service"
|
||||
@@ -69,7 +76,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 --clear --python "$BORG_PYTHON" /opt/borg1-venv
|
||||
$STD uv venv --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 +85,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 --clear --python "$BORG_PYTHON" /opt/borg2-venv
|
||||
$STD uv venv --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
|
||||
@@ -103,37 +110,9 @@ function update_script() {
|
||||
$STD uv venv --python "$BORG_PYTHON" /opt/borg-ui/.venv
|
||||
$STD uv pip install --python /opt/borg-ui/.venv -r requirements.txt
|
||||
msg_ok "Updated Python Environment"
|
||||
fi
|
||||
|
||||
if [[ -f /opt/borg-ui/packaging/native/start.sh ]] && ! grep -q 'packaging/native/start.sh' /etc/systemd/system/borg-ui.service; then
|
||||
msg_info "Switching Service to the upstream start script"
|
||||
cat <<EOF >/etc/systemd/system/borg-ui.service
|
||||
[Unit]
|
||||
Description=Borg-UI
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/borg-ui
|
||||
EnvironmentFile=/opt/borg-ui/.env
|
||||
Environment=BORG_UI_VENV=/opt/borg-ui/.venv
|
||||
ExecStart=/bin/bash /opt/borg-ui/packaging/native/start.sh
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
msg_ok "Switched Service to the upstream start script"
|
||||
changed=1
|
||||
fi
|
||||
|
||||
if [[ "$changed" == 1 ]]; then
|
||||
msg_info "Starting Service"
|
||||
systemctl restart borg-ui
|
||||
systemctl start borg-ui
|
||||
msg_ok "Started Service"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
|
||||
Loaded 100 of 1768 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user