Compare commits

...
Author SHA1 Message Date
MickLesk d8416ee904 pocketbase-bot: apply website metadata requests from Accept/Decline boxes
The website now posts each requested change as its own comment with the
command and two checkboxes. Ticking one edits the comment: Accept runs
the command as if a maintainer had typed it, Decline records the
refusal, and the issue closes once every change is decided. Only users
with write access can decide. app_vars is checked field by field before
it is saved.
2026-11-12 11:53:31 +01:00
Rögl-Brunner Michel 3e9ce91252 pr-script-test: test the update of apps deployed from a branch too 2026-11-12 11:29:12 +01:00
Rögl-Brunner Michel 03ea5280e6 CI hosts: single-use, reset before every job 2026-11-12 11:11:19 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 9b6c24f8ff Update CHANGELOG.md (#18627)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 08:54:34 +00:00
push-app-to-main[bot]andCanbiZ cbb347cc61 Pixelfed (#18523)
* Add pixelfed (ct)

* arm64 unknown

* pixelfed: use nginx_enable_site, enable FFI for vips, align .env with upstream

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-12 09:54:10 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 0ad290c81f Update CHANGELOG.md (#18626)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 08:53:15 +00:00
CanbiZ (MickLesk) 0b25f9c6a5 Whisparr-eros: set the first login and save it (#18617)
* fix(whisparr-eros): set the first login and save it

Eros 3.6 turns on forms login and creates the user whisparr with a password that only appears in its log, so a fresh install showed a login nobody knew the password for (#18616). The install now sets a random password through WHISPARR__AUTH__INITIALPASSWORD for the first start only, removes it again, and saves the login in ~/whisparr-eros.creds; the password stays out of the logs.

* whisparr-eros: set the first password in the unit, write creds with cat
2026-11-12 09:52:47 +01:00
Rögl-Brunner Michel 1ac6100001 pr-script-test: build every PR automatically, forks included 2026-11-12 09:22:03 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 46f54a9071 Update CHANGELOG.md (#18625)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-12 08:13:14 +00:00
Rögl-Brunner Michel d67f53176e pr-script-test: /test can pick the platforms, and takes app names in PRs 2026-11-12 09:12:45 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 1edcadd90b Update CHANGELOG.md (#18623)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 22:53:34 +00:00
CanbiZ (MickLesk) b1597ac3d1 Paperless-AI: install and update to 4.0 (#18620)
* Paperless-AI: install and update to 4.0

4.0 is a rewrite as a single Node.js process. requirements.txt is gone,
so every install since its release on 2026-11-10 failed at pip, and an
update would have failed the same way.

Install and update now build it the way upstream's image does. An
update from 3.x removes the Python RAG service and rewrites the unit
without data/.env as its environment: 4.0 migrates that file on first
start and treats any setting passed as a variable as locked.

* Paperless-AI: migrate pre-RAG 3.x units too, drop comments
2026-11-11 23:53:08 +01:00
CanbiZ (MickLesk) 0f2e4022c0 Immich: bump to 3.4.1 (#18619) 2026-11-11 21:39:28 +01:00
juronja bf5fa77f0b TrueNas: Fixing isolookup and dialog (#18578)
* fix(truenas-vm): improve ISO fetching logic and exclusions

* fix(truenas-vm): enhance ISO selection and error handling

* fix(truenas-vm): improve ISO selection and error handling

* fix(truenas-vm): enhance ISO lookup and selection process to filter by stable releases only and improve user dialog
2026-11-11 21:33:09 +01:00
Rögl-Brunner Michel fb58720888 arm-runner-setup: get past the runner's DNS failing for the arm64 mirror
The ARM runner's resolver cannot resolve ports.ubuntu.com since this
afternoon; apt then hangs. The name is now looked up over DNS-over-HTTPS
when needed, and a failing apt prints the runner's network state.
2026-11-11 16:24:38 +01:00
Rögl-Brunner Michel 7258873339 arm-runner-setup: time limits, so a hung setup can't hold the report 2026-11-11 16:03:49 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 243c3b1047 Update CHANGELOG.md (#18612)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 14:24:52 +00:00
CanbiZ (MickLesk) 5a4f599f4f Fill platforms when a script moves from DevScripts (#18611)
Setting is_dev to false left platforms as DevScripts had them, usually empty or pve only, although a container in ProxmoxVE is synced to Incus from then on. 36 production records had no platforms because of it. The migration now sets pve and incus for containers, pve where nothing was set, and amd64 for an empty architectures.
2026-11-11 15:24:20 +01:00
Rögl-Brunner Michel 6163037afd pr-script-test: every ct/ script on ARM64 too, as information only
- ARM runs on GitHub's ubuntu-24.04-arm runner (free for public repos), set
  up by arm-runner-setup.sh: Incus 6.0 LTS from Zabbly (key pinned, APT
  pin), screenshots through Playwright. ARM rows are shown but never count:
  not in the headline, the check, the review or the approval, and the job
  runs with continue-on-error.
- var_arm64=no is not built on ARM; unset is built with var_arm64=yes.
- arm-script-test.yml builds one script on ARM by hand, posting nothing.
- Update test: only the versions update_script checks are set back.
- The screenshot browser gets a writable HOME.
2026-11-11 13:31:33 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 95abb0a2d6 Update CHANGELOG.md (#18610)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 12:26:01 +00:00
CanbiZ (MickLesk) 76996d2d82 Pocketbase: install and update from the new host (removed in github) (#18609)
* fix(pocketbase): install and update from the new host

PocketBase moved from GitHub to ganigeorgiev.com in October 2026; the GitHub repo is archived at v0.40.6 and gets no new releases. Install and update now read the latest stable tag from the new release API and download the zip from its fixed download path. The update no longer runs pocketbase update, which older binaries point at GitHub.

* fix(pocketbase): query the release API with curl_with_retry
2026-11-11 13:25:36 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 009c7b0c0e Update CHANGELOG.md (#18608)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 11:30:22 +00:00
Rögl-Brunner Michel 01f37f501f pr-script-test: build vm/ scripts, and every ct/ script on Incus too
- vm/ scripts run unattended (VM_UNATTENDED=1) on the Proxmox hosts, IDs
  9100-9199, one VM per host at a time. Checks: success line, the VM keeps
  running, a DHCP lease, and a ${VM_IP} URL answering 200. Console
  screenshot from QEMU when there is no web UI. ARM64-only scripts are
  listed as not built.
- Every ct/ script now builds on pve and incus (new incus-test runners,
  incus-ci-host-setup.sh); the comment gets a Platform column.
- Spinner frames are dropped from logs and live output.
- The sweeper ignores and removes locks of killed jobs (older than 3h).
2026-11-11 11:48:57 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] b60ff8540a Update CHANGELOG.md (#18606)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 10:30:42 +00:00
CanbiZ (MickLesk) 3aca471c7b Immich: bump to 3.4.0 (#18600)
3.4.0 adds FUTO Backups, which run restic from PATH and use restic-proxy when it is there. Both are built from the refs the server Dockerfile pins, and an update only rebuilds them when those change. pigz is installed, which database backups now use for compression when present. mise skips its own restic, and the extism-js fallback follows the js-pdk version in mise.toml (v1.7.0) instead of a fixed v1.6.0.
2026-11-11 11:30:11 +01:00
Rögl-Brunner Michel f63d1bd425 pr-script-test: test the update too, and /test <app> in issues
Update test: after a passing install, every version file the install
wrote (~/.<app>) is set back to 0.0.0-ci and the container's own
`update` runs unattended, pulling the PR's ct/ script, so
update_script's real path is exercised. It must report "Updated
successfully" without errors, and services and URL are checked again.
Apps without a version file get "nothing to update", a GitHub rate
limit counts as not tested. Separate Install and Update columns in the
report; a failed update requests changes like a failed install.

The job's read-only token is passed to the build so release lookups are
not rate limited (all test containers share one public IP).

/test <app> in an issue builds that app from main, install and update,
and answers in the issue; no check run, review or approval.
2026-11-11 10:46:01 +01:00
Rögl-Brunner Michel d859b2f764 pr-script-test: runners named after their host
A second test host (pve-ci-2) now serves both repositories. The setup
script registered runners as pve-ci-<repo> with --replace, which would
have made it take over the first host's registration; they are now
named <host>-<repo>.
2026-11-11 10:30:43 +01:00
Michel Roegl-Brunner 81e8e0495b Increase timeout from 60 to 90 minutes 2026-11-11 09:41:57 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ebd69e822d Update CHANGELOG.md (#18604)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-11 08:18:37 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] c1e45c1c51 Update CHANGELOG.md (#18599)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 18:36:57 +00:00
Rögl-Brunner Michel e1dd96614d pr-script-test: approve migrations only when they change nothing else
Anyone with write access can push to a push-app-to-main branch, and the
bot's approval counts toward the main ruleset's required review. The
approval now only covers what a migration contains: if the PR changes
anything besides ct/*.sh and install/*.sh (workflows, misc/, ...), it is
tested as usual but not approved, and the result comment says why.
2026-11-10 16:34:29 +01:00
Rögl-Brunner Michel cfada6ed6a pr-script-test: run on DevScripts migrations by itself, approve passes
PRs that the push-app-to-main app opens on a branch of this repository
(scripts moving over from DevScripts, reviewed and tested there,
writable only by writers) are now built on pull_request_target for
every push, pinned to the pushed commit; an event for an older push is
ignored. When every script passes, the bot approves the tested commit,
which counts toward the main ruleset's required review; a new push
drops it as stale, and a later run that does not fully pass withdraws
it. Untestable (CPU) and setup errors are never approved. All other
PRs still need /test and are never approved.
2026-11-10 16:32:40 +01:00
Rögl-Brunner Michel cc27caf470 pr-script-test: say why nothing was built
A /test on a PR that only changes vm/ (#18578, TrueNAS) found nothing
to build and stopped without a word. The gate now always answers:
"Nothing to build" with the reason for each file (vm/ not covered,
tools/ not built, install/ script without a ct/ script of the same
name, more than 10 scripts), and when some scripts are built the rest
are listed as "Not built" in the progress and the result comment.
File names from the PR are put in code spans with backticks and line
breaks removed.
2026-11-10 16:24:59 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] dc9d75a5ad Update CHANGELOG.md (#18597)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 15:22:34 +00:00
Michel Roegl-Brunner 79481f3a74 pr-script-test: pre-answer prompt variables built in a loop (#18596)
OpenCloud reads var_opencloud_${server}_fqdn for each server in
servers=("opencloud" "collabora" "wopi"), so the scan for literal
var_ names found nothing and the build stopped at the first prompt
(#18586). Templates of the form var_...${x}... are now expanded with
the words of `for x in ...`, or of the array that loop walks; nothing
is executed. Hostname-like answers are derived from the variable name
(ci-opencloud-collabora.example.test), so several cannot collide.
2026-11-10 16:22:06 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] bd7f6f60bd Update CHANGELOG.md (#18595)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 15:05:16 +00:00
Michel Roegl-Brunner db30bcd960 Build PR scripts on a Proxmox test host via /test (#18594)
A maintainer comments `/test` on a pull request and a self-hosted
runner on a disposable Proxmox VE host builds every ct/ script the PR
touches (and the ct/ entry point of every changed install/ script),
checks the result and reports back on the PR.

- Gate: only users with write access; builds the latest commit only if
  nothing was pushed to the fork after the comment, or a pinned
  `/test <full sha>`.
- Build: the PR tree at that commit is served from the test host with
  every raw.githubusercontent.com/community-scripts/ProxmoxVE/main URL
  rewritten to it, so engine changes in the PR are tested too.
  Unattended (mode=default); prompts declared the AGENTS.md way are
  pre-answered with CI values.
- Checks: build and install succeed, container running, no failed
  units, the URL the script prints answers 200. Screenshot of that URL.
- Report: check run, one comment (results, screenshot, install log,
  mentions of the PR author and whoever asked), changes requested on a
  real failure, dismissed again on a pass. CPU-feature failures (no
  AVX2 on the test host) are marked untestable, not failed.
- The test job has a read-only token and no secrets; its runner carries
  only the proxmox-test label. Container IDs 9050-9099.

Same file as in DevScripts; github.repository selects the URLs and the
container ID range.
2026-11-10 16:04:32 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] cba9a593cf Update CHANGELOG.md (#18591)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 09:40:40 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 9fa4751635 Update CHANGELOG.md (#18590)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 09:35:05 +00:00
Michel Roegl-Brunner aba63a4e3b Force symbolic link update for writefreely (#18588) 2026-11-10 10:34:34 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 7b985d5ed8 Update CHANGELOG.md (#18589)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 09:28:16 +00:00
CanbiZ (MickLesk) aa219cff0b fix(openproject): start memcached when IPv6 is disabled (#18584)
Debian 13's memcached also listens on ::1 and cannot start in a container with IPv6 disabled, so openproject configure failed (#18579). adventurelog had the same memcached silently down. Both drop the ::1 listener.
2026-11-10 10:27:48 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d367da8fcb Update CHANGELOG.md (#18585)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 09:08:55 +00:00
Zajsoft 2880722c71 Update pip install command for runlike (#18583) 2026-11-10 10:08:29 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 641cc9ab1d Update CHANGELOG.md (#18582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-10 08:08:26 +00:00
Christian BenincasaandClaude Opus 5.5 17cf76f6ee Tunarr: fix install and update for the new release layout and latest ErsatzTV-ffmpeg (#18577)
* Tunarr: drop binary rename for the new release folder layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tunarr: track the latest ErsatzTV-ffmpeg and link ffmpeg into /usr/bin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 09:07:56 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] c120fb6f9f Update CHANGELOG.md (#18575)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 16:27:30 +00:00
CanbiZ (MickLesk) bf3bd50926 security: remove hardcoded credentials in several scripts (#18572) 2026-11-09 17:27:03 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] cfeb555f8b Update CHANGELOG.md (#18571)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 13:41:43 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] af8f922610 Update CHANGELOG.md (#18570)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 13:19:39 +00:00
github-actions[bot] 08854f9b1b chore(ct): sync zipline defaults from PocketBase (#18569)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-11-09 14:19:13 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 293a0a85c1 Update CHANGELOG.md (#18567)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 12:27:09 +00:00
bfba6d1f30 OpenObserve: record installed version, drop killall ExecStop (#18566)
update_script never wrote ~/.openobserve, so check_for_gh_release reported
an update on every run and reinstalled the release. Write the version after
extraction, as other ct scripts do, and overwrite instead of append at install.

The unit ExecStop=killall failed with 203/EXEC because psmisc is never
installed. OpenObserve treats SIGQUIT and SIGTERM alike, so drop ExecStop
and remove it from existing units during update.

Co-authored-by: BadFlo <7882184+BadFlo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:26:43 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 2166dfc210 Update CHANGELOG.md (#18565)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 12:08:44 +00:00
7a0faa4f94 OpenObserve: extract release tarball with --no-same-owner (#18564)
The release tarball stores the binary as uid/gid 1001. Extracted as root,
GNU tar keeps that owner, so /opt/openobserve/openobserve ended up owned by
a non-existent uid after install and every update.

Co-authored-by: BadFlo <7882184+BadFlo@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 13:08:19 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] f4c8bff028 Update CHANGELOG.md (#18563)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 11:40:50 +00:00
Michel Roegl-Brunner 81f14b0a02 Delete ct/coolify.sh (#18562) 2026-11-09 12:40:20 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 19b6cbc34e Update CHANGELOG.md (#18560)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 09:17:10 +00:00
Ulaş Kalaycı eb2f583793 yuvomi: install from the attested release asset (#18554) 2026-11-09 10:16:37 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 8d50af5c69 Update CHANGELOG.md (#18559)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 08:59:45 +00:00
github-actions[bot]andCanbiZ 2b63e9da14 cinephage: bump Node.js from 24 to 26 (#18374)
* cinephage: bump Node.js from 24 to 26

* Fix command to fetch Camoufox in install script

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-09 09:59:18 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 0283df4573 Update CHANGELOG.md (#18558)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 08:46:47 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] d8087c2c0e Update CHANGELOG.md (#18557)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 08:42:33 +00:00
Tim MooreandClaude Opus 5.5 90d80949c1 Immich: download before stopping, and clear maintenance mode after an interrupted update (#18553)
* fix(immich): download before stopping, and clear maintenance mode after an interrupted update

setup_uv and the Immich release fetch ran after maintenance mode, the service stop and rm -rf app/*, so a failed download (e.g. a GitHub releases/download 504) left the container with no app at all. They now run first, while the old install is still serving.

The fetch writes the new version to ~/.immich, so the old version is put back until the update finishes and a failed run can still be retried.

Maintenance mode is now disabled whenever immich-admin exists at the end, not only when this run enabled it. Since #17798 a re-run after an interrupted update skips the enable, so it skipped the disable too and left the web UI on the maintenance page.

* fix(immich): trim comments to the AGENTS.md limit

* fix(immich): rely on the core pending marker, and only clear maintenance mode this run owns

fetch_and_deploy_release already keeps ~/.immich.pending until a clean exit and redoes the update while it exists, so the hand-rolled version swap is dropped; PREV_IMMICH now reads CS_PREVIOUS_VERSION so a redo sees the real old version.

Maintenance mode is disabled when this run enabled it, or when app/ was empty at the start (an interrupted run left it on), rather than whenever immich-admin exists.

* immich: drop comment per review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:42:03 +01:00
CanbiZ (MickLesk) 2f0f6daee8 Update Node.js version to 26 in cinephage.sh 2026-11-09 09:40:29 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] cdae3b974f Update CHANGELOG.md (#18556)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 08:39:39 +00:00
mazeladoandMatt Daleo 50d1c199f5 IP-Tag: filter guest-agent IPs by CIDR before limiting to 3 (#18537)
Co-authored-by: Matt Daleo <12395090+mazelado@users.noreply.github.com>
2026-11-09 09:39:13 +01:00
github-actions[bot] f4e0f5508f cross-seed: bump Node.js from 24 to 26 (#17083)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-11-09 09:34:16 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 492575699c Update CHANGELOG.md (#18555)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-09 08:16:26 +00:00
github-actions[bot]andCanbiZ 623d26a39d kan: bump Node.js from 20 to 24 (#18549)
* kan: bump Node.js from 20 to 24

* Set NODE_VERSION and NODE_MODULE before setup

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-09 09:16:02 +01:00
github-actions[bot]andCanbiZ 191ab7e861 myip: bump Node.js from 24 to 26 (#18550)
* myip: bump Node.js from 24 to 26

* Increase default RAM allocation from 1024MB to 2048MB

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-09 09:15:51 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ba43fb009b Update CHANGELOG.md (#18545)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 20:53:58 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 9c04500924 Update CHANGELOG.md (#18544)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 20:53:49 +00:00
CanbiZ (MickLesk) d6d5996837 fix(update-apps): keep the backup alive on a closed console, stop before restoring (#18540)
A closed console killed vzdump with the session (7 runs ended "Backup failed ... creating vzdump archive"); it now runs shielded from SIGHUP. A restore after a failed update raced the background shutdown and failed with "can't overwrite running container" (3 runs); it now waits for that shutdown and for the container to stop, and says so when it does not.
2026-11-08 21:53:34 +01:00
CanbiZ (MickLesk) d4ab2d465f fix(clean-lxcs): run with sh and keep going when one container fails (#18538)
Alpine containers without bash failed with 127, and a failed apt update or pct start ended the whole run (12 runs in three days). The cleanup now runs with sh, keeps the apt lists so a failed update does not leave none, and lists the failed containers at the end.
2026-11-08 21:53:28 +01:00
CanbiZ (MickLesk) 31666305dc fix(update-lxcs): keep going when one container fails (#18541)
A failed patchmon-agent report, apk upgrade or pct start ended the whole run under set -e (18 runs in three days). The run now updates the other containers, lists the failed ones at the end and exits 1.
2026-11-08 21:53:26 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 84ba70bc1c Update CHANGELOG.md (#18543)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 17:04:05 +00:00
CanbiZ (MickLesk) 2032fa8175 fix(immich): do not clone libjxl's testdata (#18539)
The build runs with BUILD_TESTING=OFF, but the recursive submodule update also cloned the large testdata repo, and two installs died on network errors inside it. Only third_party is fetched now; libjxl at Immich's pinned revision configures and builds without it.
2026-11-08 18:03:40 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d4de03f8e8 Update CHANGELOG.md (#18542)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 16:06:59 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] b457333b72 Update CHANGELOG.md (#18536)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 13:50:39 +00:00
push-app-to-main[bot]andCanbiZ 06e30b9c4f Chatto (#18521)
* Add chatto (ct)

* rm old Header comments

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-08 14:50:13 +01:00
push-app-to-main[bot]andCanbiZ 8390e7e524 Firefox-Syncserver (#18520)
* Add firefox-syncserver (ct)

* cleanup

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-08 14:50:06 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 81441221ad Update CHANGELOG.md (#18534)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 13:04:55 +00:00
CanbiZ (MickLesk) d65cf17de4 fix(databasus): build 3.65 and ship the verification agent (#18500)
3.65 moved the entry point to cmd/databasus and embeds the API spec and migrations, so swag failed and every install since 2026-11-05 broke. It now builds like the upstream image: bundled client tools, the verification agent for amd64 and arm64, and APP_VERSION set to the release. Valkey, goose and the client symlinks are gone because 3.65 uses none of them. Closes #18493.
2026-11-08 14:04:30 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 8127d1caea Update CHANGELOG.md (#18531)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 00:03:47 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6eb9414ee7 Archive old changelog entries (#18530)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-08 00:03:21 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] a60d5882d3 Update CHANGELOG.md (#18528)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:36:10 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 292561163c Update CHANGELOG.md (#18527)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:33:19 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 76aa58d6c4 Update CHANGELOG.md (#18526)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:33:02 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4442360cd9 Update CHANGELOG.md (#18525)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:05:59 +00:00
adotagarwal 7bc2815df7 RomM: emit player isolation headers from the serving location (#18511)
The player location's COOP/COEP add_header never fires: try_files
internally redirects /rom/<id>/ejs to /index.html, served by
"location /" — so SharedArrayBuffer cores (PSP/DS) fail with a
misleading "must be run over HTTPS" error.

Apply upstream RomM's own fix (docker nginx template): map
$request_uri — which survives the redirect — to header values for the
ejs and jsdos player paths, emit them from "location /", and drop the
dead player location. Also restore upstream's Cache-Control "no-cache"
on the SPA shell, without which a cached index.html hides the headers
and stale frontends survive RomM updates.

Widen the updater's migration gate so existing installs converge.
2026-11-07 21:05:35 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 1e722e8bd7 Update CHANGELOG.md (#18524)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:01:03 +00:00
push-app-to-main[bot]andCanbiZ 91e9cc699c EspoCRM (#18519)
* Add espocrm (ct)

* minor fixes

* minor

---------

Co-authored-by: push-app-to-main[bot] <203845782+push-app-to-main[bot]@users.noreply.github.com>
Co-authored-by: CanbiZ (MickLesk) <47820557+MickLesk@users.noreply.github.com>
2026-11-07 21:00:38 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] fbbbc55699 Update CHANGELOG.md (#18518)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 19:54:21 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6ad67ca11e Update CHANGELOG.md (#18517)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 19:53:59 +00:00
Tobias 68c1309517 fix: gotenberg: update to v9 (#18491) 2026-11-07 20:53:53 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 53c32f293b Update CHANGELOG.md (#18508)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 20:53:36 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 1d5fdefc4e Update CHANGELOG.md (#18509)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-07 00:40:43 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 6a0c9b002d Update CHANGELOG.md (#18507)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 22:51:59 +00:00
CanbiZ (MickLesk) 81d218adc2 fix(umbrel-os-vm): report runs as ProxmoxVE, not DevScripts (#18497)
The script kept the DevScripts base URL from the refactor in #17505, so every production run was filed under DevScripts in telemetry.
2026-11-06 23:51:57 +01:00
CanbiZ (MickLesk) 125c2c44a7 ci: reject DevScripts URLs in scripts (#18498)
openwrt-vm and umbrel-os-vm kept the DevScripts base URL when they moved over, and telemetry filed their production runs under DevScripts. The check fails a pull request that adds such a URL to ct, install, vm or tools.
2026-11-06 23:51:35 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d63c98936b Update CHANGELOG.md (#18506)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 22:50:56 +00:00
CanbiZ (MickLesk) df10e1d2dd Technitium DNS: read the version from its GitHub releases (#18425)
The version came from scraping technitium.com, which failed for some hosts ("Command substitution exited with status 1", 6 runs on 4 hosts in three days) and has no rate-limit fallback. The GitHub release carries the same version and goes through the engine's release lookup.
2026-11-06 23:50:54 +01:00
CanbiZ (MickLesk) 50c5b786e4 Caddy: skip the xCaddy test build below 2 GB RAM (#18472)
* fix(caddy): skip the xCaddy test build below 2 GB RAM

The container defaults to 512 MB (256 MB on Alpine), and Go's linker was
killed in xcaddy build: 14 runs on 6 hosts in a week. The build only
produced an unused binary, so below 2 GB xCaddy is installed without it
and a warning says why; install and update alike.

* Change directory to /opt before building xCaddy
2026-11-06 23:50:49 +01:00
CanbiZ (MickLesk) 3c0e693c2d fix(thingsboard): keep the package defaults in thingsboard.conf (#18499)
The install replaced the file, which dropped JAVA_OPTS with install.data_dir, so creating a tenant failed. The DB settings are now appended, and an update restores the defaults on existing containers. The package upgrade keeps the edited file. Fixes #18483.
2026-11-06 23:50:29 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d8f3874f42 Update CHANGELOG.md (#18505)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 22:47:00 +00:00
CanbiZ (MickLesk) 1a2bbb06bb fix(nexterm): keep the extracted app out of the /tmp cleanup (#18501)
The server unpacks its frontend into /tmp/nexterm-sea-*. systemd-tmpfiles removes it after 10 days while the process keeps running, and every page returns 404. Fixes #18488.
2026-11-06 23:46:31 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 996900f145 Update CHANGELOG.md (#18504)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 22:42:46 +00:00
CanbiZ (MickLesk) bc348949c4 fix(planka): run the service with node instead of nodemon (#18503)
npm 12 rejects --prod, and --production still starts PLANKA through nodemon. Starting node app.js directly needs HOME: Sails 1.5.18 only looks for the project's .sailsrc when HOME is set, and systemd sets none without User=, so the app lifted with the wrong paths and returned 503. The unit now sets NODE_ENV and HOME, and an update repairs existing units even without a new release. Follow-up to #18477.
2026-11-06 23:42:25 +01:00
CanbiZ (MickLesk) 5720358d1b MeTube/Yubal: install Deno without its shell setup (#18406)
* MeTube: install Deno without its shell setup

With -y, deno's install.sh also fetches jsr:@deno/installer-shell-setup to edit the shell profile, and when jsr.io did not answer the install stopped ("JSR package manifest ... failed to load"). Deno goes to /usr/local/bin, which is on PATH, so the shell setup is not needed; CI=1 keeps the installer from asking for it on a terminal.

* Modify Deno installation command

Update Deno installation command to include CI environment variable.
2026-11-06 23:42:22 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ebf9cf656e Update CHANGELOG.md (#18502)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 18:32:26 +00:00
CanbiZ (MickLesk) c5c406b392 OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated (#18404)
* OPNsense VM: size the temp space check to the unpacked image

The script refused to run unless TEMP_DIR had 20 GB free, a figure left
over from when the download went there too. The download now goes to the
image cache and only the unpacked FreeBSD image (2.5 GB) lands in
TEMP_DIR, so hosts with 8 to 16 GB free in /tmp stopped with exit 214.
Check the size xz reports for the image instead.

* OPNsense VM: say why the EFI disk could not be allocated

pvesm's error went to stderr only, so the two exit 220 runs in telemetry
carry no reason. As msg_error it reaches the log and the trace, e.g.
"disk image '.../vm-969-disk-0.qcow2' already exists" after an earlier
run left its disk on a storage that qm destroy does not clean.

* Clean up comments in opnsense-vm.sh

Removed comments about image cache and size calculation.
2026-11-06 19:31:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 745043bd9a Update CHANGELOG.md (#18490)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-06 07:58:59 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] c0df6c9870 Update CHANGELOG.md (#18486)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:22:30 +00:00
CanbiZ (MickLesk) 5e5e840458 fix(openwrt-vm): leave the NIC untagged on a bridge without a port (#18473)
Advanced mode prefills the LAN VLAN with 999. On a bridge with no
physical port that is not VLAN-aware (an internal vmbr1, the usual LAN
choice) pve-bridge cannot tag the tap, and qm start failed with "no
physical interface on bridge 'vmbr1'". The tag is dropped there with a
warning, for LAN and WAN alike.

The scripts base still pointed at DevScripts, where this script was
removed on 10-09, so every run was reported as a DevScripts run.
2026-11-05 21:21:58 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4adef93eb9 Update CHANGELOG.md (#18485)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:21:25 +00:00
CanbiZ (MickLesk) 94aa8e7ab0 fix(podman): skip Portainer where Podman has no Quadlet (#18469)
Portainer and the agent are written as Quadlet .container units, which
need Podman 4.4. Debian 12 ships 4.3.1, so systemctl start portainer
failed with "Unit portainer.service not found". Without Quadlet both
prompts are skipped with a warning.
2026-11-05 21:20:55 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] eb7d180f9f Update CHANGELOG.md (#18484)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 20:18:12 +00:00
CanbiZ (MickLesk) 6e6ca04f24 truenas-VM: let a failed ISO lookup reach its own error (#18476)
* fix(truenas-vm): let a failed ISO lookup reach its own error

When download.truenas.com did not answer, the lookup handed an empty
path to an associative array (bad array subscript) and the pipeline
failed under pipefail, so the run ended with exit 1 at the SELECTED_ISO
line instead of the "Could not find a stable ISO" message. The index
fetch now fails on HTTP errors and retries, empty lines are skipped, and
the assignment no longer aborts on an empty result.

* fix(truenas-vm): use the engine's retry flags for the ISO index
2026-11-05 21:17:45 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] ce04c99aa2 Update CHANGELOG.md (#18482)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 16:23:05 +00:00
CanbiZ (MickLesk) df705aefc4 proxmox-mail-gateway: take the repo suite from the container (#18475)
* fix(proxmox-mail-gateway): take the repo suite from the container

The install hardcoded trixie, so a Debian 12 container got the PMG 9
repository and pmg-api could not resolve its trixie dependencies (held
broken packages). Suite and key now follow the container's codename;
bookworm still has PMG 8 with proxmox-mailgateway-container.

* fix(proxmox-mail-gateway): read the codename inline
2026-11-05 17:22:36 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 4fd7aa7edb Update CHANGELOG.md (#18481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 15:32:06 +00:00
CanbiZ (MickLesk) c55556144a Immich: Bump to 3.3.6 and minor improvement (#18474)
* fix(immich): take the watchfiles wheel and retry GeoNames longer

The v3.3.5 lock pins watchfiles 0.21.0, which has no wheel for the
Python 3.13 machine-learning venv, so every ML install downloaded a Rust
toolchain and compiled it. Where static.rust-lang.org reset the
connection, uv sync failed three times. --upgrade-package watchfiles
takes 1.3.0, an abi3 wheel; only uvicorn's reload mode uses it.

download.geonames.org dropped connections on 11-03/04 and the first file
failed after three 10 s connects; those four calls now retry six times
with a 30 s connect timeout.

* Immich: bump release version to v3.3.6

v3.3.6 disables onnxruntime's telemetry in immich_ml, which runs from
source here, so nothing else changes. The lock still pins watchfiles
0.21.0, so the wheel upgrade stays.
2026-11-05 16:31:37 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 43481c25fd Update CHANGELOG.md (#18480)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 14:25:17 +00:00
CerberusStyle d2970719e4 Change npm start argument to --production (#18478)
* Change npm start argument to --production

* update service with --production

* Reload systemd daemon after updating service file
2026-11-05 15:24:44 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 215f9be228 Update CHANGELOG.md (#18468)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-05 10:57:03 +00:00
CanbiZ (MickLesk) 3e7fcf1350 Arcane: write both env names, stop shipping the unused .env (#18466)
The compose file from main now uses ARCANE_ENCRYPTION_KEY and
ARCANE_PROJECTS_DIRECTORY, which the latest image (2.17.3) does not read,
so it panicked on a missing ENCRYPTION_KEY. Arcane 3 in turn refuses to
start when only the old names are set. Install and update now keep both
names in compose.yaml, which also carries existing installs over to 3.

The .env was never referenced by the compose file, but compose parses it,
and the unanchored ENCRYPTION_KEY sed turned the renamed line into
ARCANE_#ENCRYPTION_KEY and broke every install. New installs no longer
download it; update repairs the broken line.
2026-11-05 11:56:39 +01:00
CanbiZ (MickLesk) 8ba3582919 OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds (#18467)
The FreeBSD image boots in under a minute when both NICs get DHCP, but
dhclient waits 60 seconds on each NIC without a DHCP server, which is the
normal case for a firewall's LAN before setup. On vx-pve an isolated LAN
took 110 seconds; the reporter of #18464 measured 210. The keys then went
into the boot output. The image prints its login prompt on the serial
console as well, so the script now reads serial0 and continues when it
appears, up to 15 minutes, or waits the old 3 minutes if the console
cannot be read.
2026-11-05 11:56:32 +01:00
CanbiZ (MickLesk) 37e4a33615 Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 (#18465)
Tududi 2.0.0 dropped SQLite and refuses to start next to a populated 1.x
database. New installs set up PostgreSQL and let start.sh create the
schema; db:init would sync the tables without SequelizeMeta and make
db-prepare treat the database as an old 1.x one. Installs without
DATABASE_URL stay on 1.15.0, the final SQLite release, which also brings
back installs that already updated to 2.0.0.

Uploads lived in backend/uploads, which CLEAN_INSTALL wiped on every
update; they move to /opt/tududi-uploads.
2026-11-05 11:56:29 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 3e54356653 Update CHANGELOG.md (#18462)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 22:59:18 +00:00
Chris Thach 1f03150b77 fix: Poll Teleport readyz instead of fixed 10 second sleep. (#18461)
Signed-off-by: Chris Thach <git.identify957@passfwd.com>
2026-11-04 23:58:49 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] cfa250a795 Update CHANGELOG.md (#18459)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 16:07:16 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] bb4c1f2fbf Update CHANGELOG.md (#18458)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 16:00:26 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 793fba1a53 Update CHANGELOG.md (#18457)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 15:54:56 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 06b788ee81 Update CHANGELOG.md (#18454)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 09:23:17 +00:00
CanbiZ (MickLesk) 667da6a8fb Install n8n past its unsatisfiable canvas dependencies (#18439)
n8n-nodes-base 2.46.0 moved @napi-rs/canvas-linux-arm64-musl and
-linux-x64-musl from optional into hard dependencies. npm enforces
os/cpu/libc on those, and no combination satisfies both arm64 and x64,
so every install fails with EBADPLATFORM. n8n 2.46.4 reached the latest
tag on 2026-11-03 and the failures started four minutes later.

--force skips the platform check. The matching gnu binary is still
installed through @napi-rs/canvas's own optional dependencies, so canvas
keeps working.
2026-11-04 10:22:46 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] e40a258a27 Update CHANGELOG.md (#18452)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-04 07:04:53 +00:00
CanbiZ (MickLesk) ff5aac3889 Teable: add Redis cache (fixes #18449) (#18450) 2026-11-04 08:04:23 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] e00d14a3ce Update CHANGELOG.md (#18445)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 15:45:50 +00:00
CanbiZ (MickLesk) 5ee75b7460 Wiki.js: install 3.x (#18382)
* Wiki.js: install 3.x

Wiki.js 3 lives in requarks/wiki, needs Node.js 26 and creates its own ltree and pg_trgm extensions in the wiki schema, and starts with node --no-experimental-webstorage backend. dataPath moves out of the app folder, which every update replaces; the admin password is generated and saved to ~/wikijs.creds. Updates follow upstream's guide and put Puppeteer back when it was installed. 2.x containers keep getting 2.x updates from requarks/wiki-v2, since 3.x cannot upgrade them in place, and are told how to export and import.

* Refactor credential output to use heredoc syntax
2026-11-03 16:45:19 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 069ab1b8df Update CHANGELOG.md (#18444)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:59:39 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 209eb2a003 Update CHANGELOG.md (#18443)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:59:24 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 594b1e82bb Update CHANGELOG.md (#18442)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:59:05 +00:00
CanbiZ (MickLesk) bffc871aa9 Flag instead of closing on first PR template miss; add script-request eligibility check (#18384)
close-invalid-pr-template.yml now comments and labels (missing pr template,
stale) instead of closing immediately, clears itself once fixed. stale_pr_close.yml
gains the engagement-gated/keep-open/maintainer-reset behavior already used in
DevScripts, so a template-stale PR only closes via its own re-check.

request-script.yml discussion template gets an optional Justification field;
a new workflow verifies stars/age against GitHub for the submitted repo and
either does nothing (eligible), labels investigation (ineligible + justified),
or closes the discussion (ineligible, no justification).

Moved the AI Assistance section to the end of pull_request_template.md.
2026-11-03 14:58:57 +01:00
CanbiZ (MickLesk) d77c65093b Say why an update-apps backup or restore failed (#18405)
vzdump and pct restore ran with their output thrown away, so a failed
backup only said "Backup failed for container N" (exit 235). That is the
second most common update-apps failure and none of them can be told
apart. Keep the output in the run log and put its error line in the
message, e.g. "CT is locked (backup)".

var_backup_storage was matched with grep -w, so "truenas" passed when
only "truenas-backup" exists and vzdump then failed on a storage that
does not exist. Match the whole name, and list the valid ones in the
error instead of a spinner line after it.

Name the app in "Update failed for container N", so a failed update
says which app's update failed. Declining the start dialog now exits 0
like the other tools instead of failing with whiptail's 1.
2026-11-03 14:58:35 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] f51d21ff9a Update CHANGELOG.md (#18441)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:32:03 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 27493df7a0 Update CHANGELOG.md (#18440)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:31:40 +00:00
CanbiZ (MickLesk)andTobias 0973d1ccfb OpenWrt VM: read the stable release from downloads.openwrt.org (#18411)
* OpenWrt VM: read the stable release from downloads.openwrt.org

The version came from a line of text on the openwrt.org wiki front page.
One host got that page without the line three times in a row while
others resolved fine at the same moment, and the run ended with exit 115
before anything was downloaded. .versions.json on the download server
is what the OpenWrt firmware selector reads and names the stable release
explicitly.

* Update vm/openwrt-vm.sh

Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>

---------

Co-authored-by: Tobias <96661824+CrazyWolf13@users.noreply.github.com>
2026-11-03 14:31:32 +01:00
CanbiZ (MickLesk) 11acd8db75 Keep host tools running when clear does not know the terminal (#18412)
clear exits 1 for a TERM the host has no terminfo entry for, such as
xterm-ghostty or xterm-kitty over SSH, and when TERM is unset under cron.
The tools that run under errexit died on it in header_info within a
second, before showing anything, which telemetry counts as
"exit_code=1 | General error" with no step for update-lxcs, update-repo,
fstrim, clean-lxcs, execute and storage-share-helper.

Fall back to the escape sequence the engine's _cs_clear uses.
2026-11-03 14:30:57 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] e39450ec48 Update CHANGELOG.md (#18438)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:26:52 +00:00
CanbiZ (MickLesk) 2c4fbc01eb all-templates: answering No or Cancel is not a failure (#18423)
Both whiptail prompts ran bare under errexit, so No on the first question
or Cancel in the template list ended in the error handler and was reported
as a failed run (3 of 5 failures in three days).

Monitor-All is now stopped right before the download instead of at the
start, so leaving at a prompt no longer leaves it stopped.
2026-11-03 14:26:31 +01:00
CanbiZ (MickLesk) dd7a659f22 homebrew: skip service accounts when picking the user, survive a failed apt update (#18424)
The user was the first passwd entry with a UID from 1000 up. On a Proxmox
host with Ceph that is ceph (UID 64045, shell nologin), and su - ceph fails
with "This account is currently not available". Accounts without a login
shell are skipped now.

apt update on a host with the enterprise repository and no subscription exits
100 on the 401. apt_update_safe retries, says why and carries on, as the other
scripts already do since #17462.
2026-11-03 14:26:23 +01:00
CanbiZ (MickLesk) f9039c2ee7 Fedora VM: let the engine install libguestfs-tools (#18422)
The script ran its own apt-get update before installing
libguestfs-tools, so one broken or stale repository on the host (a 404
for a hand-added proxmox.com or debian.org entry) ended the run with
exit 100. vm_prepare_cloud_image installs the tools itself and carries
on when an update fails, so the install can still succeed from the
repositories that did update.
2026-11-03 14:26:21 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 44666826df Update CHANGELOG.md (#18437)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:24:46 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 64a89e3463 Update CHANGELOG.md (#18436)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:24:19 +00:00
CanbiZ (MickLesk) 0d4e703d70 Cinephage: run the camoufox binary the package now ships (#18426)
Cinephage depends on camoufox instead of camoufox-js, whose bin is called camoufox, so the install stopped at "node_modules/.bin/camoufox-js: No such file or directory" (6 runs in three days).
2026-11-03 14:24:13 +01:00
CanbiZ (MickLesk) 5fa9846d37 Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first (#18397)
- VectorChord moved to supervc-stack/VectorChord. With the GitHub API
  rate limited, core reads the release from github.com, finds no asset
  under the old name and stops with "No suitable .deb asset found".
- mise installed every tool in Immich's mise.toml. Java (for the Dart
  OpenAPI generator), terragrunt and opentofu are not needed for the
  plugin build, and the Java index timed out on slow links.
- GeoNames failed after the 1-3 h library build; it now downloads first.
- Stop on x86-64 CPUs without SSE4.2: the web build ends there with
  "Illegal instruction" after the libraries are built.
2026-11-03 14:23:44 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] e32161b18a Update CHANGELOG.md (#18435)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:23:29 +00:00
CanbiZ (MickLesk) 92737744ce AlmaLinux VM: report failures through the engine's error handler (#18418)
The script replaced the engine's error_handler with one that sent the
failed command as the exit code, so every failure reached telemetry as
exit 199 "exit code not reported", without the failed command or
category. Its signal traps sent words instead of codes the same way.

It also ran its own unguarded apt-get update for libguestfs-tools, so a
host with the enterprise repository and no subscription (401) stopped
there. vm_prepare_cloud_image installs the tools itself, tolerating a
repository that fails to update.
2026-11-03 14:23:03 +01:00
CanbiZ (MickLesk) 2fbb6e261c add-tailscale-lxc: start a stopped container and check its OS first (#18421)
The container menu lists stopped containers too, and pct exec on one exits
255: 12 of 41 failures in three days. Start it the way add-netbird-lxc does
and wait for its default route.

The install runs under bash, which an Alpine container from the template does
not have; lxc-attach then exits 127 before the Alpine branch is reached.
Install bash there first, and stop with a clear message on any OS other than
Debian, Ubuntu or Alpine instead of failing on apt-get.
2026-11-03 14:22:52 +01:00
CanbiZ (MickLesk) 39b3ed29ad WireGuard: give the Debian container 1 GB for WGDashboard (#18419)
WGDashboard builds its frontend with Vite during the install, and at 512 MB the kernel killed node: "Failed to compile frontend", then wg-dashboard would not start (11 runs in three days). The build finishes at 1 GB.
2026-11-03 14:20:44 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 9bdc6ae13b Update CHANGELOG.md (#18434)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:14:47 +00:00
community-scripts-pr-app[bot]andgithub-actions[bot] 9aff2f1090 Update CHANGELOG.md (#18433)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:14:34 +00:00
CanbiZ (MickLesk) d6e4427ac9 zabbix: use os specific apt packages (#18417)
* Zabbix: install on Debian 12 again

The install and the update fetched Zabbix's release package for Debian 13 and php8.4-pgsql, so a Debian 12 container stopped with "Unable to locate package php8.4-pgsql" (12 runs in three days). Zabbix publishes 7.0 and 7.4 for Debian 12 too: take the container's Debian version and the distribution's php-pgsql.

* Zabbix: leave the update as it was

The update refuses Debian 12 on purpose, so its release URL stays as it was.
2026-11-03 14:14:17 +01:00
CanbiZ (MickLesk) 770d89bb90 Refuse to run the PBS 4 upgrade on a Proxmox VE host (#18416)
Nothing checked that the host is a Backup Server. On a PVE host the
first step stopped only because tar found no /etc/proxmox-backup (exit
2); answering "no" to that backup would have gone on to delete
/etc/apt/sources.list.d/*.list, add the PBS repositories and
dist-upgrade. Stop with the same message post-pbs-install uses.
2026-11-03 14:14:11 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 3aef3bb084 Update CHANGELOG.md (#18432)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:13:16 +00:00
CanbiZ (MickLesk) e2b09a9fcd Discourse: build libvips 8.18 for its image worker (#18430)
Discourse enabled libvips image processing by default and its worker refuses anything older than 8.18; Debian 13 ships 8.16, so the service failed to boot after an update and on fresh installs. Build the latest libvips release from source like discourse_docker does, on install and on update when a new release is out.
2026-11-03 14:13:09 +01:00
CanbiZ (MickLesk) 96710d22f2 netdata: survive a failed apt update, leave quietly on Cancel (#18427)
The add-on runs on the Proxmox host, where the enterprise repository without a
subscription answers 401 and apt update exits 100 (2 of 3 failures in three
days). apt_update_safe retries, points at the enterprise repository and
carries on; the netdata repository was fetched fine.

Cancel in the first menu was reported as a failed run.
2026-11-03 14:12:57 +01:00
CanbiZ (MickLesk) cfecd0ed05 Wazuh: show the installer's own error when it fails (#18428)
wazuh-install.sh writes everything to ~/wazuh-install.output, so a failed run left only "bash exited with status 1" in the log and the telemetry (8 runs in three days, three of them with the default resources). The end of that output now goes into the install log.
2026-11-03 14:12:50 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] 480ba6d90f Update CHANGELOG.md (#18431)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 13:01:23 +00:00
CanbiZ (MickLesk) 61dcda815a Keep trimming when pct fstrim fails on one container (#18413)
pct fstrim exits 255 when the container is locked, for example while a
backup runs ("CT is locked (backup)"). Under errexit the command
substitution ended the whole run there, before the output check below it
and before the remaining containers, and telemetry filed it as
"DPKG: Fatal internal error". Print the result and go on instead.
2026-11-03 14:00:54 +01:00
CanbiZ (MickLesk) 2340816c4e Do not abort execute-lxcs on a container that is not Debian or Ubuntu (#18414)
The skip message printed $name, which is only set once a container has
run the command. When a non-Debian container (Alpine, for example) came
before any Debian one, set -u ended the run there with "name: unbound
variable" and exit 1; later it printed the previous container's name.
2026-11-03 13:59:54 +01:00
CanbiZ (MickLesk) 7700d7ce36 Offer only container-capable storages as the converter's restore target (#18415)
The target list came from storages with "images" content, but pct
restore puts the container's rootfs there and refuses a storage without
"rootdir" ("storage 'X' does not support container directories"). A
storage that holds VM disks only was offered, and picking it ended the
run after the backup with "Conversion failed" (exit 235).
2026-11-03 13:59:51 +01:00
community-scripts-pr-app[bot]andgithub-actions[bot] d701955118 Update CHANGELOG.md (#18429)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 12:54:54 +00:00
CanbiZ (MickLesk) 8dc0d17740 Let post-pve-install go on when legacy sources are kept (#18403)
On a PVE 9 host without deb822 .sources files the script asks whether to
disable the legacy .list sources, then read the answer from $? on the
next line. Under set -e a "No" ended the whole run with exit 1 right
there, so the "Kept legacy sources as-is" branch never ran and nothing
after it was offered. Test whiptail in the if instead.
2026-11-03 13:54:14 +01:00
146 changed files with 5307 additions and 1064 deletions

No files matched your search

+18 -2
View File
@@ -58,9 +58,9 @@ body:
Before continuing, verify that the requested project meets the minimum requirements below.
**Projects with fewer than 1,000 stars are generally not eligible for a script request.**
**Projects with fewer than 1,000 stars, or younger than 6 months, are generally not eligible for a script request.**
Exceptions are only considered where there is a clearly verifiable, significant public adoption signal.
An automated check verifies the star count and project age for GitHub-hosted projects. If the project doesn't meet these requirements, fill in **Justification for an exception** below — without one, the request is closed automatically. With one, it's flagged for manual review instead. Exceptions are rare and require a clearly verifiable, significant public adoption signal.
- type: input
id: repository-stars
@@ -134,6 +134,22 @@ body:
- label: I understand that requests outside the project scope may be closed without an extensive explanation.
required: true
- type: textarea
id: justification
attributes:
label: Justification for an exception
description: >-
Only needed if the project has fewer than 1,000 stars or is younger than 6 months (e.g. it has no GitHub
presence, so stars don't apply). Explain the significant public adoption signal. Leave blank if the project
meets the normal requirements — requests that don't meet the requirements and have no justification here are
closed automatically.
placeholder: |
Example:
No public GitHub repository, but the project has 50k+ active installs reported by its own telemetry
dashboard and is covered by several established self-hosting publications.
validations:
required: false
- type: markdown
attributes:
value: |
+187
View File
@@ -0,0 +1,187 @@
## 2026-11-07
### 🆕 New Scripts
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
- #### 💥 Breaking Changes
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
### 💾 Core
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
## 2026-11-06
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- UmbrelOS-VM: report runs as ProxmoxVE, not DevScripts [@MickLesk](https://github.com/MickLesk) ([#18497](https://github.com/community-scripts/ProxmoxVE/pull/18497))
- Caddy: skip the xCaddy test build below 2 GB RAM [@MickLesk](https://github.com/MickLesk) ([#18472](https://github.com/community-scripts/ProxmoxVE/pull/18472))
- Thingsboard: keep the package defaults in thingsboard.conf [@MickLesk](https://github.com/MickLesk) ([#18499](https://github.com/community-scripts/ProxmoxVE/pull/18499))
- Nexterm: keep the extracted app out of the /tmp cleanup [@MickLesk](https://github.com/MickLesk) ([#18501](https://github.com/community-scripts/ProxmoxVE/pull/18501))
- PLANKA: run the service with node instead of nodemon [@MickLesk](https://github.com/MickLesk) ([#18503](https://github.com/community-scripts/ProxmoxVE/pull/18503))
- MeTube/Yubal: install Deno without its shell setup [@MickLesk](https://github.com/MickLesk) ([#18406](https://github.com/community-scripts/ProxmoxVE/pull/18406))
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
- #### 🔧 Refactor
- Technitium DNS: read the version from its GitHub releases [@MickLesk](https://github.com/MickLesk) ([#18425](https://github.com/community-scripts/ProxmoxVE/pull/18425))
### 💾 Core
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
### 📚 Documentation
- ci: reject DevScripts URLs in scripts [@MickLesk](https://github.com/MickLesk) ([#18498](https://github.com/community-scripts/ProxmoxVE/pull/18498))
## 2026-11-05
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
- #### 🔧 Refactor
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
### 💾 Core
- fix: keep the existing install until the new archive has extracted [@rcourtman](https://github.com/rcourtman) ([core#201](https://github.com/community-scripts/core/pull/201))
- fix: keep the install when a clone or an Alpine extract fails [@rcourtman](https://github.com/rcourtman) ([core#210](https://github.com/community-scripts/core/pull/210))
### 🧰 Tools
- #### 🐞 Bug Fixes
- Arcane: write both env names, stop shipping the unused .env [@MickLesk](https://github.com/MickLesk) ([#18466](https://github.com/community-scripts/ProxmoxVE/pull/18466))
## 2026-11-04
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Teleport: Poll readyz instead of fixed 10 second sleep. [@cthach](https://github.com/cthach) ([#18461](https://github.com/community-scripts/ProxmoxVE/pull/18461))
- n8n: install past its unsatisfiable canvas dependencies [@MickLesk](https://github.com/MickLesk) ([#18439](https://github.com/community-scripts/ProxmoxVE/pull/18439))
- Teable: add Redis cache [@MickLesk](https://github.com/MickLesk) ([#18450](https://github.com/community-scripts/ProxmoxVE/pull/18450))
### 💾 Core
- feat: say why download_gpg_key failed [@MickLesk](https://github.com/MickLesk) ([core#208](https://github.com/community-scripts/core/pull/208))
- feat: read the MOTD's OS version at login [@MickLesk](https://github.com/MickLesk) ([core#207](https://github.com/community-scripts/core/pull/207))
- feat: skip screen clear when there is no terminal | pin only the scripts own app with var_appversion [@MickLesk](https://github.com/MickLesk) ([core#203](https://github.com/community-scripts/core/pull/203))
## 2026-11-03
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Fedora VM: let the engine install libguestfs-tools [@MickLesk](https://github.com/MickLesk) ([#18422](https://github.com/community-scripts/ProxmoxVE/pull/18422))
- Cinephage: run the camoufox binary the package now ships [@MickLesk](https://github.com/MickLesk) ([#18426](https://github.com/community-scripts/ProxmoxVE/pull/18426))
- AlmaLinux VM: report failures through the engine's error handler [@MickLesk](https://github.com/MickLesk) ([#18418](https://github.com/community-scripts/ProxmoxVE/pull/18418))
- WireGuard: give the Debian container 1 GB for WGDashboard [@MickLesk](https://github.com/MickLesk) ([#18419](https://github.com/community-scripts/ProxmoxVE/pull/18419))
- zabbix: use os specific apt packages [@MickLesk](https://github.com/MickLesk) ([#18417](https://github.com/community-scripts/ProxmoxVE/pull/18417))
- Discourse: build libvips 8.18 for its image worker [@MickLesk](https://github.com/MickLesk) ([#18430](https://github.com/community-scripts/ProxmoxVE/pull/18430))
- Wazuh: show the installer's own error when it fails [@MickLesk](https://github.com/MickLesk) ([#18428](https://github.com/community-scripts/ProxmoxVE/pull/18428))
- Tracearr: move to Node.js 26 [@connorgallopo](https://github.com/connorgallopo) ([#18393](https://github.com/community-scripts/ProxmoxVE/pull/18393))
- #### ✨ New Features
- Wiki.js: install 3.x [@MickLesk](https://github.com/MickLesk) ([#18382](https://github.com/community-scripts/ProxmoxVE/pull/18382))
- #### 🔧 Refactor
- OpenWrt VM: read the stable release from downloads.openwrt.org [@MickLesk](https://github.com/MickLesk) ([#18411](https://github.com/community-scripts/ProxmoxVE/pull/18411))
- Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first [@MickLesk](https://github.com/MickLesk) ([#18397](https://github.com/community-scripts/ProxmoxVE/pull/18397))
### 💾 Core
- fix: put the reason for a VM failure into its trace [@MickLesk](https://github.com/MickLesk) ([core#200](https://github.com/community-scripts/core/pull/200))
- fix: check the bridge before creating a container, and its state after start [@MickLesk](https://github.com/MickLesk) ([core#199](https://github.com/community-scripts/core/pull/199))
- ref: offer to remove only the container this run created [@MickLesk](https://github.com/MickLesk) ([core#198](https://github.com/community-scripts/core/pull/198))
- feat: report a cancelled tool dialog as an abort and name the failing command [@MickLesk](https://github.com/MickLesk) ([core#197](https://github.com/community-scripts/core/pull/197))
- feat: check KVM and the host's threads before building a VM [@MickLesk](https://github.com/MickLesk) ([core#196](https://github.com/community-scripts/core/pull/196))
- ref: give a host tool one log file for the whole run [@MickLesk](https://github.com/MickLesk) ([core#194](https://github.com/community-scripts/core/pull/194))
- fix: keep a piped stdin when output moves to the log after a hangup [@MickLesk](https://github.com/MickLesk) ([core#193](https://github.com/community-scripts/core/pull/193))
- fix: pve-LXC: say when the container went away during its setup [@MickLesk](https://github.com/MickLesk) ([core#192](https://github.com/community-scripts/core/pull/192))
- fix: treat Esc in a VM wizard dialog as leaving the script [@MickLesk](https://github.com/MickLesk) ([core#190](https://github.com/community-scripts/core/pull/190))
- ref: count a container stopped during a detached install as an abort [@MickLesk](https://github.com/MickLesk) ([core#187](https://github.com/community-scripts/core/pull/187))
- feat: find the assets of a renamed GitHub repo without the API [@MickLesk](https://github.com/MickLesk) ([core#188](https://github.com/community-scripts/core/pull/188))
- fix: restore npm when an interrupted update left it behind [@MickLesk](https://github.com/MickLesk) ([core#189](https://github.com/community-scripts/core/pull/189))
- feat: add var_cmode to set the container console mode [@MickLesk](https://github.com/MickLesk) ([core#180](https://github.com/community-scripts/core/pull/180))
### 🧰 Tools
- #### 🐞 Bug Fixes
- all-templates: answering No or Cancel is not a failure [@MickLesk](https://github.com/MickLesk) ([#18423](https://github.com/community-scripts/ProxmoxVE/pull/18423))
- homebrew: skip service accounts when picking the user, survive a failed apt update [@MickLesk](https://github.com/MickLesk) ([#18424](https://github.com/community-scripts/ProxmoxVE/pull/18424))
- add-tailscale-lxc: start a stopped container and check its OS first [@MickLesk](https://github.com/MickLesk) ([#18421](https://github.com/community-scripts/ProxmoxVE/pull/18421))
- netdata: survive a failed apt update, leave quietly on Cancel [@MickLesk](https://github.com/MickLesk) ([#18427](https://github.com/community-scripts/ProxmoxVE/pull/18427))
- Privilege-Converter: offer only container-capable storages as the converter's restore target [@MickLesk](https://github.com/MickLesk) ([#18415](https://github.com/community-scripts/ProxmoxVE/pull/18415))
- let post-pve-install go on when legacy sources are kept [@MickLesk](https://github.com/MickLesk) ([#18403](https://github.com/community-scripts/ProxmoxVE/pull/18403))
- #### 🔧 Refactor
- update-apps: say why an backup or restore failed [@MickLesk](https://github.com/MickLesk) ([#18405](https://github.com/community-scripts/ProxmoxVE/pull/18405))
- Keep host tools running when clear does not know the terminal [@MickLesk](https://github.com/MickLesk) ([#18412](https://github.com/community-scripts/ProxmoxVE/pull/18412))
- Refuse to run the PBS 4 upgrade on a Proxmox VE host [@MickLesk](https://github.com/MickLesk) ([#18416](https://github.com/community-scripts/ProxmoxVE/pull/18416))
- fstrim: keep trimming when pct fstrim fails on one container [@MickLesk](https://github.com/MickLesk) ([#18413](https://github.com/community-scripts/ProxmoxVE/pull/18413))
- execute-lxc: do not abort on a container that is not Debian or Ubuntu [@MickLesk](https://github.com/MickLesk) ([#18414](https://github.com/community-scripts/ProxmoxVE/pull/18414))
### 📚 Documentation
- github: flag instead of closing on first PR template miss; add script-request eligibility check [@MickLesk](https://github.com/MickLesk) ([#18384](https://github.com/community-scripts/ProxmoxVE/pull/18384))
## 2026-11-02
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- FileFlows: replace the program folders on update [@MickLesk](https://github.com/MickLesk) ([#18380](https://github.com/community-scripts/ProxmoxVE/pull/18380))
- anchor: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18373](https://github.com/community-scripts/ProxmoxVE/pull/18373))
- guardian: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18375](https://github.com/community-scripts/ProxmoxVE/pull/18375))
- nametag: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18376](https://github.com/community-scripts/ProxmoxVE/pull/18376))
### 💾 Core
- github: trim theater, add signal to the engine bug report; move AI block to end of PR template [@MickLesk](https://github.com/MickLesk) ([core#186](https://github.com/community-scripts/core/pull/186))
- fix: do not wait forever for verbose output a daemon keeps open [@MickLesk](https://github.com/MickLesk) ([core#185](https://github.com/community-scripts/core/pull/185))
## 2026-11-01
### 🆕 New Scripts
- Amazon Linux ([#18369](https://github.com/community-scripts/ProxmoxVE/pull/18369))
- CachyOS VM ([#18368](https://github.com/community-scripts/ProxmoxVE/pull/18368))
- Razzia ([#18366](https://github.com/community-scripts/ProxmoxVE/pull/18366))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- fix(zerobyte): build the bundled local agent [@nicotsx](https://github.com/nicotsx) ([#18363](https://github.com/community-scripts/ProxmoxVE/pull/18363))
+10 -10
View File
@@ -17,16 +17,6 @@ Fixes #
**Tested on:** <!-- e.g. PVE 9.2 / Debian 13 / fresh install + update. Write "not tested" if you could not run it. -->
---
## 🤖 AI Assistance (**X** in brackets)
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
> Select exactly one option.
- [ ] **No AI used** – Scripts were written without AI assistance.
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/DevScripts/blob/main/.github/agents/pve-script-creator.agent.md) as guidance, and the output has been reviewed and corrected to match those guidelines.
---
## 🛠️ Type of Change (**X** in brackets)
@@ -70,3 +60,13 @@ Guidance:
-->
<!-- The advisory block is only active once it is OUTSIDE this comment. -->
---
## 🤖 AI Assistance (**X** in brackets)
> If you used an AI tool (GitHub Copilot, Claude, ChatGPT, etc.) to write or generate any scripts in this PR, you **must** confirm compliance below.
> Select exactly one option.
- [ ] **No AI used** – Scripts were written without AI assistance.
- [ ] **AI was used** – I confirm the scripts were built using [`AGENTS.md`](https://github.com/community-scripts/DevScripts/blob/main/AGENTS.md) and [`.github/agents/pve-script-creator.agent.md`](https://github.com/community-scripts/DevScripts/blob/main/.github/agents/pve-script-creator.agent.md) as guidance, and the output has been reviewed and corrected to match those guidelines.
+114
View File
@@ -0,0 +1,114 @@
name: ARM script test (manual)
# Builds one ct/ script on GitHub's free arm64 runner (ubuntu-24.04-arm),
# inside Incus, with the same harness as pr-script-test.yml. Started by hand
# from the Actions tab; it posts nothing anywhere: the result is in the job
# summary and the artifact (with the screenshots).
#
# pr-script-test.yml runs the same ARM build for every /test, as information
# only. This one is for trying a script or branch on ARM without a PR.
#
# Public repositories get the standard hosted runners, arm64 included, at no
# cost. Each job is a fresh VM that is thrown away afterwards, so the script
# under test never touches the lab.
#
# The engine refuses to guess on arm64: a script that leaves var_arm64 unset
# stops with "refusing to guess". Here var_arm64=no means "not built", yes is
# built as is, and unset is built with var_arm64=yes to see whether it would
# work.
on:
workflow_dispatch:
inputs:
script:
description: "ct/ script, e.g. ct/bark-server.sh"
required: true
type: string
ref:
description: "Branch or full SHA of this repository"
required: false
default: main
type: string
permissions:
contents: read
jobs:
arm:
runs-on: ubuntu-24.04-arm
timeout-minutes: 120
steps:
# The harness from the default branch, as in pr-script-test.yml.
- uses: actions/checkout@v5
with:
sparse-checkout: .github/workflows/scripts
persist-credentials: false
- name: Decide var_arm64
id: plan
env:
SCRIPT: ${{ inputs.script }}
REF: ${{ inputs.ref }}
GH_TOKEN: ${{ github.token }}
run: |
[[ "$SCRIPT" =~ ^ct/[A-Za-z0-9._-]+\.sh$ ]] || { echo "::error::script must be ct/<name>.sh"; exit 1; }
[[ "$REF" =~ ^[A-Za-z0-9._/-]+$ ]] || { echo "::error::bad ref"; exit 1; }
sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${REF}" --jq .sha)
text=$(gh api "repos/${GITHUB_REPOSITORY}/contents/${SCRIPT}?ref=${sha}" --jq .content | base64 -d)
# A commented-out line ("#var_arm64=...") counts as unset.
arm=$(grep -m1 -oE '^[[:space:]]*var_arm64="[$][{]var_arm64:-(yes|no)[}]"' <<<"$text" | grep -oE ':-(yes|no)' | cut -c3- || true)
echo "sha=$sha" >>"$GITHUB_OUTPUT"
echo "arm64=${arm:-unset}" >>"$GITHUB_OUTPUT"
case "$arm" in
no) echo "::notice::${SCRIPT} sets var_arm64=no: not built on ARM" ;;
yes) echo "${SCRIPT} sets var_arm64=yes" ;;
*) echo "::notice::${SCRIPT} leaves var_arm64 unset: building with var_arm64=yes to see whether it works" ;;
esac
echo "Building ${SCRIPT} at ${sha:0:7} on $(uname -m), $(nproc) cores, $(free -g | awk '/^Mem/ {print $2}') GB RAM, $(df -h / | awk 'NR==2 {print $4}') free"
- name: Set up Incus
if: steps.plan.outputs.arm64 != 'no'
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
- name: Build and check ${{ inputs.script }}
if: steps.plan.outputs.arm64 != 'no'
env:
PLATFORM: incus
VAR_ARM64: "yes"
PR_REPO: ${{ github.repository }}
PR_SHA: ${{ steps.plan.outputs.sha }}
SCRIPT: ${{ inputs.script }}
UPSTREAM: ${{ github.repository }}
CTID_BASE: "9000"
GH_API_TOKEN: ${{ github.token }}
OUT_DIR: ${{ runner.temp }}/result
BUILD_TIMEOUT: 90m
run: sudo --preserve-env=PLATFORM,VAR_ARM64,PR_REPO,PR_SHA,SCRIPT,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,BUILD_TIMEOUT,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT bash .github/workflows/scripts/pve-run-script.sh
- name: Summary
if: always() && steps.plan.outputs.arm64 != 'no'
env:
OUT_DIR: ${{ runner.temp }}/result
ARM64: ${{ steps.plan.outputs.arm64 }}
run: |
sudo chmod -R a+rX "$OUT_DIR" 2>/dev/null || true
r="$OUT_DIR/result.json"
[[ -f "$r" ]] || { echo "No result.json" >>"$GITHUB_STEP_SUMMARY"; exit 0; }
{
echo "### ARM (arm64, Incus): $(jq -r .script "$r")"
echo
echo "| Status | Install | Update | Duration | var_arm64 | Detail |"
echo "|---|---|---|---|---|---|"
jq -r --arg arm "$ARM64" '"| \(.status) | \(if .update_status != "" then "pass" else .status end) | \(if .update_status == "" then "-" else .update_status end) | \(.duration)s | \($arm) | \(if .status == "pass" then (.url // "" | if . == "" then "no URL" else . + " → " + $ARGS.named.c end) else .step end) |"' --arg c "$(jq -r .http_code "$r")" "$r"
echo
ls "$OUT_DIR"/screenshot*.png &>/dev/null && echo "Screenshots: in the \`result-arm\` artifact." || echo "No screenshot."
} >>"$GITHUB_STEP_SUMMARY"
- if: always()
uses: actions/upload-artifact@v4
with:
name: result-arm
path: ${{ runner.temp }}/result
if-no-files-found: ignore
retention-days: 14
+27
View File
@@ -0,0 +1,27 @@
name: Check Script Repo URL
on:
pull_request:
branches: ["main"]
paths:
- "ct/**"
- "install/**"
- "vm/**"
- "tools/**"
permissions:
contents: read
jobs:
check:
if: github.repository == 'community-scripts/ProxmoxVE'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Reject DevScripts URLs
run: |
if grep -rnE 'community-scripts/(ProxmoxVED|DevScripts)/' ct install vm tools; then
echo "::error::These lines point at DevScripts, so telemetry files the runs there. Use community-scripts/ProxmoxVE."
exit 1
fi
+44 -40
View File
@@ -1,4 +1,4 @@
name: Close PRs Missing Template
name: Flag PRs Missing Template
on:
pull_request_target:
@@ -14,10 +14,12 @@ jobs:
issues: write
contents: read
steps:
- name: Close PR if it does not follow the PR template
- name: Check PR against the template, comment and label instead of closing
uses: actions/github-script@v9
with:
script: |
const MARKER = "<!-- pr-template-check -->";
const LABELS = ["missing pr template", "stale"];
const pr = context.payload.pull_request;
const prNumber = pr.number;
const author = pr.user.login;
@@ -44,11 +46,11 @@ jobs:
return;
}
const labels = pr.labels.map((label) => label.name);
const prLabels = pr.labels.map((label) => label.name);
const skipLabels = ["automated pr", "keep-open"];
if (skipLabels.some((label) => labels.includes(label))) {
core.info(`PR #${prNumber} has a skip label (${labels.join(", ")}) — skipping template validation.`);
if (skipLabels.some((label) => prLabels.includes(label))) {
core.info(`PR #${prNumber} has a skip label (${prLabels.join(", ")}) — skipping template validation.`);
return;
}
@@ -114,50 +116,52 @@ jobs:
failures.push("At least one **Type of Change** checkbox must be checked.");
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber, per_page: 100
});
const mine = comments.find(c => c.body && c.body.includes(MARKER));
if (failures.length === 0) {
core.info(`PR #${prNumber} follows the PR template.`);
if (mine && !mine.body.startsWith(MARKER + "\n✅")) {
await github.rest.issues.updateComment({
owner, repo, comment_id: mine.id,
body: MARKER + "\n✅ **Template check passed.** Thanks for fixing the description — the `stale` mark has been removed.",
});
}
for (const name of LABELS) {
if (prLabels.includes(name)) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name });
}
}
return;
}
core.info(`Closing PR #${prNumber} — template validation failed.`);
core.info(`PR #${prNumber} — template validation failed, flagging instead of closing.`);
const templateUrl =
"https://github.com/community-scripts/ProxmoxVE/blob/main/.github/pull_request_template.md";
const failureList = failures.map((item) => `- ${item}`).join("\n");
const comment = [
`👋 Hi @${author},`,
``,
`This pull request was closed because it does not follow the [PR template](${templateUrl}).`,
``,
`Please fix the following and open a new PR (or reopen this one after updating the description):`,
``,
failureList,
``,
`> Use the template sections, fill in the description, check all prerequisite boxes, and select at least one type of change.`,
``,
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.`,
``,
`Thank you for contributing! 🙏`,
].join("\n");
const text =
MARKER + "\n" +
`👋 Hi @${author},\n\n` +
`This pull request does not follow the [PR template](${templateUrl}) yet:\n\n` +
failureList + "\n\n" +
`**Edit the PR description** to fix it — no new PR needed. This check runs again on every edit and clears itself once everything is in place.\n\n` +
`Until then the PR is marked \`stale\` and will be closed in **7 days**.\n\n` +
`Maintainers can add the \`keep-open\` label to exempt a PR from this check.\n\n` +
`Thank you for contributing! 🙏`;
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: comment,
});
if (mine) {
if (mine.body !== text) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body: text });
}
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body: text });
}
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
state: "closed",
});
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ["missing pr template"],
});
const toAdd = LABELS.filter(name => !prLabels.includes(name));
if (toAdd.length > 0) {
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: toAdd });
}
+13 -6
View File
@@ -230,7 +230,7 @@ jobs:
for (const slug of slugs) {
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1&expand=type', {
headers: { 'Authorization': token }
});
const list = JSON.parse(listRes.body);
@@ -239,14 +239,21 @@ jobs:
console.log('Slug not in DB, skipping: ' + slug);
continue;
}
const patch = {
name: record.name || record.slug,
last_update_commit: prUrl,
is_dev: false
};
// Containers in ProxmoxVE are synced to Incus from here on.
const recordType = record.expand && record.expand.type && record.expand.type.type;
const platforms = Array.isArray(record.platforms) ? record.platforms : [];
if (recordType === 'lxc' && !platforms.includes('incus')) patch.platforms = ['pve', 'incus'];
else if (!platforms.length) patch.platforms = ['pve'];
if (!Array.isArray(record.architectures) || !record.architectures.length) patch.architectures = ['amd64'];
const patchRes = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: record.name || record.slug,
last_update_commit: prUrl,
is_dev: false
})
body: JSON.stringify(patch)
});
if (!patchRes.ok) {
console.warn('PATCH failed for slug ' + slug + ': ' + patchRes.body);
+149 -4
View File
@@ -2,7 +2,7 @@ name: PocketBase Bot
on:
issue_comment:
types: [created]
types: [created, edited]
permissions:
issues: write
@@ -25,6 +25,9 @@ jobs:
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
COMMENT_BODY: ${{ github.event.comment.body }}
COMMENT_ID: ${{ github.event.comment.id }}
EVENT_ACTION: ${{ github.event.action }}
SENDER: ${{ github.event.sender.login }}
PREVIOUS_BODY: ${{ github.event.changes.body.from }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || 'false' }}
REPO_OWNER: ${{ github.repository_owner }}
@@ -87,7 +90,7 @@ jobs:
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
const isPrComment = process.env.IS_PR_COMMENT === 'true';
const commentId = parseInt(process.env.COMMENT_ID, 10);
const actor = process.env.ACTOR;
let actor = process.env.ACTOR;
function ghRequest(path, method, body) {
const headers = {
@@ -278,7 +281,65 @@ jobs:
// Accept /pocketbase at the start of ANY line (leading whitespace ok),
// so the command works even when preceded by other text. Mid-sentence
// mentions and blockquoted ("> ...") examples are ignored.
const commentBody = process.env.COMMENT_BODY || '';
let commentBody = process.env.COMMENT_BODY || '';
// ── Metadata requests from the website ─────────────────────────────
// One comment per requested change, each with Accept/Decline boxes.
// Ticking one edits the comment; that edit is the only event handled.
const REQUEST_MARKER = '<!-- pb-request -->';
const DONE_MARKER = '<!-- pb-request:done -->';
const isRequest = commentBody.includes(REQUEST_MARKER);
let decidedBy = null;
if (process.env.EVENT_ACTION === 'edited' && !isRequest) process.exit(0);
if (isRequest) {
if (process.env.EVENT_ACTION !== 'edited' || commentBody.includes(DONE_MARKER)) process.exit(0);
const ticked = (body, word) =>
new RegExp('^[ \\t]*[-*] \\[[xX]\\] ' + word + '[ \\t]*\\r?$', 'm').test(body);
const previous = process.env.PREVIOUS_BODY || '';
const accept = ticked(commentBody, 'Accept') && !ticked(previous, 'Accept');
const decline = ticked(commentBody, 'Decline') && !ticked(previous, 'Decline');
if (accept === decline) process.exit(0);
const sender = process.env.SENDER;
const permRes = await ghRequest(
'/repos/' + owner + '/' + repo + '/collaborators/' + encodeURIComponent(sender) + '/permission'
);
const level = permRes.ok ? JSON.parse(permRes.body).permission : 'none';
if (level !== 'admin' && level !== 'write') {
await postComment(
'❌ **PocketBase Bot**: @' + sender + ' is not authorized to decide metadata requests.\n' +
'Only maintainers with write access can accept or decline them.'
);
process.exit(0);
}
const heading = (commentBody.match(/^### (.+)$/m) || [])[1] || 'this change';
const command = commentBody.match(/<!-- pb-command -->\r?\n([\s\S]*?)\r?\n<!-- \/pb-command -->/);
const decided = commentBody
.replace(/^[ \t]*[-*] \[[ xX]\] (Accept|Decline)[ \t]*\r?\n?/gm, '')
.replace(
REQUEST_MARKER,
'**' + (accept ? '✅ Accepted' : '❌ Declined') + '** by @' + sender + '\n\n' +
REQUEST_MARKER + '\n' + DONE_MARKER
);
const markRes = await ghRequest(
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId, 'PATCH', { body: decided }
);
if (!markRes.ok) console.warn('Could not mark the request as decided:', markRes.body);
if (decline) {
await postComment('❌ **PocketBase Bot**: **' + heading + '** declined by @' + sender + '.');
process.exit(0);
}
if (!command) {
await postComment('❌ **PocketBase Bot**: no command found in the request for **' + heading + '**.');
process.exit(0);
}
commentBody = command[1];
decidedBy = sender;
actor = sender;
}
const cmdLine = commentBody
.split('\n')
.map(l => l.trim())
@@ -292,7 +353,7 @@ jobs:
// ── Permission check ───────────────────────────────────────────────
const association = process.env.ACTOR_ASSOCIATION;
if (association !== 'OWNER' && association !== 'MEMBER') {
if (!decidedBy && association !== 'OWNER' && association !== 'MEMBER') {
await addReaction('-1');
await postComment(
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
@@ -957,6 +1018,35 @@ jobs:
);
process.exit(0);
}
if (fieldName === 'app_vars') {
const TYPES = ['text', 'password', 'number', 'boolean', 'select'];
const problems = [];
if (!Array.isArray(parsedJson)) problems.push('must be a JSON array');
else parsedJson.forEach(function (v, i) {
const at = 'entry ' + (i + 1) + ': ';
if (!v || typeof v !== 'object' || Array.isArray(v)) { problems.push(at + 'must be an object'); return; }
if (typeof v.name !== 'string' || !/^var_[a-z0-9_]+$/.test(v.name)) problems.push(at + '`name` must look like `var_example`');
if (typeof v.label !== 'string' || !v.label.trim()) problems.push(at + '`label` is required');
if (TYPES.indexOf(v.type) === -1) problems.push(at + '`type` must be one of ' + TYPES.join(', '));
if (v.default !== undefined && typeof v.default !== 'string') problems.push(at + '`default` must be a string');
if (v.type === 'select') {
const opts = v.options;
if (!Array.isArray(opts) || opts.length === 0 || opts.some(function (o) { return typeof o !== 'string' || !o; })) {
problems.push(at + 'a select needs `options`, a non-empty array of strings');
} else if (v.default !== undefined && opts.indexOf(v.default) === -1) {
problems.push(at + '`default` must be one of its options');
}
}
['required', 'secret'].forEach(function (k) {
if (v[k] !== undefined && typeof v[k] !== 'boolean') problems.push(at + '`' + k + '` must be true or false');
});
});
if (problems.length > 0) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: `app_vars` was not saved.\n\n- ' + problems.join('\n- '));
process.exit(0);
}
}
setPayload[fieldName] = parsedJson;
} else {
setPayload[fieldName] = codeBlockValue;
@@ -1236,3 +1326,58 @@ jobs:
});
ENDSCRIPT
shell: bash
- name: Close a metadata request once every change is decided
if: always() && github.event.action == 'edited' && contains(github.event.comment.body, '<!-- pb-request -->')
env:
ISSUE_NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
node << 'ENDSCRIPT'
(async function () {
const api = 'https://api.github.com/repos/' + process.env.REPO + '/issues/' + process.env.ISSUE_NUMBER;
const headers = {
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
'Accept': 'application/vnd.github+json',
'Content-Type': 'application/json',
'User-Agent': 'pocketbase-bot'
};
const https = require('https');
function call(method, url, body) {
return new Promise(function (resolve, reject) {
const req = https.request(url, { method: method, headers: headers }, function (res) {
let data = '';
res.on('data', function (d) { data += d; });
res.on('end', function () { resolve({ ok: res.statusCode < 300, status: res.statusCode, body: data }); });
});
req.on('error', reject);
if (body) req.write(JSON.stringify(body));
req.end();
});
}
const requests = [];
for (let page = 1; page <= 10; page++) {
const res = await call('GET', api + '/comments?per_page=100&page=' + page);
if (!res.ok) throw new Error('Could not list comments: ' + res.status);
const comments = JSON.parse(res.body);
for (const c of comments) {
if ((c.body || '').includes('<!-- pb-request -->')) requests.push(c.body);
}
if (comments.length < 100) break;
}
const open = requests.filter(function (b) { return !b.includes('<!-- pb-request:done -->'); });
if (requests.length === 0 || open.length > 0) {
console.log(open.length + ' of ' + requests.length + ' changes still undecided.');
return;
}
const accepted = requests.filter(function (b) { return b.includes('✅ Accepted'); }).length;
const res = await call('PATCH', api, { state: 'closed', state_reason: accepted > 0 ? 'completed' : 'not_planned' });
if (!res.ok) throw new Error('Could not close the issue: ' + res.status);
console.log('Closed: ' + accepted + ' accepted, ' + (requests.length - accepted) + ' declined.');
})().catch(function (e) {
console.error(e.message || e);
process.exit(1);
});
ENDSCRIPT
shell: bash
+780
View File
@@ -0,0 +1,780 @@
name: PR script test
# Builds the ct/ and vm/ scripts a pull request touches on real Proxmox and Incus hosts and
# reports the outcome on the PR, as a check run and as one comment kept up to
# date.
#
# Every push to a non-draft PR is built automatically, forks included: the
# maintainers decided (2026-11-12) that the lab hosts run contributors' code
# unattended. They are isolated, hold no secret, and are treated as
# compromised anyway; the sweeper and fresh guests per build keep them usable.
# Someone with write access can also comment `/test` (optionally followed by
# a full SHA to pin, the scripts to build, and the platforms to build on) to
# build again or pick what to build. Only a commit that existed when the
# comment was written gets built: a push after it is never covered, not even
# one that lands in the seconds before this job runs.
#
# Approval stays narrow: only PRs the push-app-to-main app opens on a branch
# of this repository (a script moving over from DevScripts, reviewed and
# tested there) are approved when every script passes. Every other PR gets
# the report, the check, and "changes requested" on a real failure.
#
# gate ubuntu-latest who asked, which scripts, which commit
# test proxmox-test / build + checks, one matrix job per script, platform
# incus-test / (pve, incus, arm) and OS; arm runs on GitHub's
# ubuntu-24.04-arm arm64 runner and is information only
# report ubuntu-latest check run + PR comment
#
# The test job gets a read-only token and never sees a secret: the host it runs
# on executes untrusted code and has to be treated as compromised.
#
# This file is identical in DevScripts and ProxmoxVE; github.repository decides
# which URLs get rewritten and which container ID range is used.
on:
issue_comment:
types: [created]
# Every PR, see above. pull_request_target runs this file from main, never
# the PR's copy of it, and does not wait for "approve and run" on forks.
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- "ct/**"
- "install/**"
- "vm/**"
workflow_dispatch:
inputs:
pr:
description: "Pull request number"
required: true
type: number
sha:
description: "Pin a commit (full 40-character SHA); empty = the latest, if nothing was pushed since"
required: false
type: string
scripts:
description: "Space-separated app names, ct/ or vm/ paths, and platforms (pve, incus, arm); empty = everything the PR touches, everywhere"
required: false
type: string
permissions: {}
# One queue per PR. A newer push replaces a run that is still waiting (its
# gate would skip anyway: the head moved), but never kills one that is
# building: a cancelled job leaves its guest behind until the sweeper. Any
# comment starts this workflow; one that is not a /test gets a group of its
# own, so it cannot push a waiting test out of the queue.
concurrency:
group: >-
pr-script-test-${{ github.event_name == 'issue_comment' && !startsWith(github.event.comment.body, '/test')
&& format('noop-{0}', github.run_id)
|| github.event.pull_request.number || github.event.issue.number || inputs.pr }}
cancel-in-progress: false
jobs:
gate:
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/test')) ||
(github.event_name == 'pull_request_target' && !github.event.pull_request.draft)
runs-on: ubuntu-latest
permissions:
actions: read # when the run was dispatched, for a /test without SHA
checks: write
contents: read
issues: write
pull-requests: write
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
pr: ${{ steps.plan.outputs.pr }}
repo: ${{ steps.plan.outputs.repo }}
sha: ${{ steps.plan.outputs.sha }}
check_id: ${{ steps.plan.outputs.check_id }}
actor: ${{ steps.plan.outputs.actor }}
not_built: ${{ steps.plan.outputs.not_built }}
trusted: ${{ steps.plan.outputs.trusted }}
kind: ${{ steps.plan.outputs.kind }}
platforms: ${{ steps.plan.outputs.platforms }}
steps:
- id: plan
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const MAX = 10;
core.setOutput('matrix', '[]');
// Trusted: a PR the push-app-to-main app opened on a branch of this
// repository, i.e. a script moving over from DevScripts.
const TRUSTED_AUTHOR = 'push-app-to-main[bot]';
const isTrusted = p => !!p && p.user && p.user.login === TRUSTED_AUTHOR && p.user.type === 'Bot' &&
!!p.head.repo && p.head.repo.full_name === `${owner}/${repo}`;
let prNumber, requested = '';
if (context.eventName === 'pull_request_target') {
const p = context.payload.pull_request;
prNumber = p.number;
// Exactly the pushed commit; a later push starts its own run.
requested = p.head.sha;
} else if (context.eventName === 'workflow_dispatch') {
prNumber = Number(context.payload.inputs.pr);
requested = `${context.payload.inputs.sha || ''} ${context.payload.inputs.scripts || ''}`;
} else {
prNumber = context.payload.issue.number;
const body = context.payload.comment.body.trim();
if (!/^\/test(\s|$)/.test(body)) return;
// author_association hides private org members, so ask for the permission itself.
const user = context.payload.comment.user.login;
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: user });
if (!['admin', 'maintain', 'write'].includes(perm.permission)) {
core.notice(`${user} has ${perm.permission} access; /test needs write`);
return;
}
await github.rest.reactions.createForIssueComment({
owner, repo, comment_id: context.payload.comment.id, content: 'rocket',
});
requested = body.split('\n')[0].slice('/test'.length);
}
// `/test <app>` in an issue (not a PR) builds that app from the
// default branch as it is right now, e.g. to see whether a reported
// error reproduces. No PR, so no check run, no review, no approval.
const isIssue = context.eventName === 'issue_comment' && !context.payload.issue.pull_request;
let pr = null, head, headSha;
if (isIssue) {
const { data: info } = await github.rest.repos.get({ owner, repo });
const { data: branch } = await github.rest.repos.getBranch({ owner, repo, branch: info.default_branch });
headSha = branch.commit.sha;
head = { owner: { login: owner }, name: repo, full_name: `${owner}/${repo}` };
} else {
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }));
if (pr.state !== 'open' || !pr.head.repo) { core.notice('PR is closed or its fork is gone'); return; }
head = pr.head.repo;
headSha = pr.head.sha;
if (context.eventName === 'pull_request_target' && headSha !== requested) {
core.notice('Pushed to again since; the newer push has its own run');
return;
}
}
core.setOutput('kind', isIssue ? 'issue' : 'pr');
core.setOutput('trusted', !isIssue && isTrusted(pr) ? 'true' : 'false');
const MARK = '<!-- pr-script-test -->';
const sticky = async body => {
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber });
// Only our own comment: anyone can paste the marker into theirs.
const mine = comments.find(c => c.body.includes(MARK) && c.user.type === 'Bot');
if (mine) await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
else await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
};
// Which commit the approval covers. Reading pr.head.sha alone would
// build whatever was pushed between the comment and this job
// starting, i.e. code nobody looked at. Two ways to be sure:
//
// /test <sha> that exact commit, and only while it is the head. A
// full SHA: the author writes both commits, so a short
// prefix is a quick birthday collision away.
// /test the latest commit, if nothing reached the fork after
// the comment. pushed_at is set by GitHub on every push
// and read after the PR, so a push in between shows up.
// Branches of this repository belong to people with
// write access, who could build anything anyway.
const words = requested.split(/\s+/).filter(Boolean);
// Platform words pick where to build: `/test arm`, `/test pve incus
// bark-server`. None means every platform. A run on only some of
// them is reported under its own check name and never approves.
const PLATFORM = { pve: 'pve', proxmox: 'pve', incus: 'incus', arm: 'arm', arm64: 'arm', aarch64: 'arm' };
const platformOf = t => PLATFORM[t.toLowerCase()];
const only = new Set(words.map(platformOf).filter(Boolean));
const onlyList = ['pve', 'incus', 'arm'].filter(p => only.has(p));
const tokens = words.filter(t => !platformOf(t));
const isSha = t => /^[0-9a-f]{7,40}$/.test(t);
// App names (bark-server) become ct/ paths; such a name without a
// ct/ script falls back to vm/ below.
const bare = new Set();
const toPath = t => {
if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(t) || !/^[A-Za-z0-9._-]+$/.test(t)) return t;
const p = `ct/${t.replace(/\.sh$/, '')}.sh`;
bare.add(p);
return p;
};
const approved = isIssue ? [] : tokens.filter(isSha);
let entries = (isIssue ? tokens : tokens.filter(t => !isSha(t))).map(toPath);
const refuse = async (why, retry) => {
await sticky([MARK, '### Proxmox script test', '', `⚠️ ${why} Nothing was built.`, '', retry].join('\n'));
core.notice(why);
};
if (isIssue) {
// The default branch is trusted; only which app is needed.
if (entries.length === 0) {
await refuse('In an issue, `/test` needs to know which app.', 'Comment `/test <app>`, for example `/test bark-server` (the name of its `ct/` script).');
return;
}
} else if (approved.length > 1) {
await refuse('`/test` takes at most one commit.', 'Comment `/test` to build the latest commit.');
return;
}
if (approved.length === 1 && approved[0] !== headSha) {
await refuse(
approved[0].length !== 40 && headSha.startsWith(approved[0])
? 'A short SHA is not enough, it has to be all 40 characters.'
: `\`${approved[0]}\` is not the head of this PR (any more).`,
'Comment `/test` to build the latest commit.');
return;
}
if (!isIssue && approved.length === 0 && head.full_name !== `${owner}/${repo}`) {
const askedAt = context.eventName === 'workflow_dispatch'
? (await github.rest.actions.getWorkflowRun({ owner, repo, run_id: context.runId })).data.created_at
: context.payload.comment.created_at;
const { data: fork } = await github.rest.repos.get({ owner: head.owner.login, repo: head.name });
if (new Date(fork.pushed_at) >= new Date(askedAt)) {
await refuse(
`Something was pushed to ${head.full_name} after the \`/test\`, so it is not certain which commit was meant.`,
'Comment `/test` again once you have looked at the latest commit.');
return;
}
}
if (entries.some(e => !/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(e))) {
await refuse(
isIssue ? 'Only app names (as in `ct/<app>.sh` or `vm/<name>.sh`) and platforms (`pve`, `incus`, `arm`) can be requested.' : 'Only app names, `ct/<name>.sh` and `vm/<name>.sh` paths, platforms (`pve`, `incus`, `arm`) and the commit can be requested.',
isIssue ? 'For example: `/test bark-server`, `/test arm bark-server` or `/test debian-vm`' : 'For example: `/test`, `/test arm`, or `/test pve incus bark-server`.');
return;
}
// Everything the PR changes that this test cannot build is named in
// the comment, so a /test never ends in silence.
const notBuilt = [];
const code = s => '`' + String(s).replace(/[`\r\n]/g, '') + '`';
if (entries.length === 0) {
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: prNumber });
const set = new Set();
for (const f of files) {
if (f.status === 'removed') continue;
let m;
if ((m = f.filename.match(/^install\/([A-Za-z0-9._-]+)-install\.sh$/))) set.add(`ct/${m[1]}.sh`);
else if (/^(ct|vm)\/[A-Za-z0-9._-]+\.sh$/.test(f.filename)) set.add(f.filename);
else if (/^(tools|turnkey)\/.*\.sh$/.test(f.filename)) notBuilt.push(`${code(f.filename)}: only \`ct/\` and \`vm/\` scripts are built`);
}
entries = [...set].sort();
}
const include = [];
const fetchScript = async p => {
try {
const res = await github.rest.repos.getContent({ owner: head.owner.login, repo: head.name, path: p, ref: headSha });
return Buffer.from(res.data.content || '', 'base64').toString('utf8');
} catch (e) { return null; }
};
for (let script of entries.slice(0, MAX)) {
let text = await fetchScript(script);
// /test debian-vm in an issue: there is no ct/debian-vm.sh, but a vm/ one.
const wasBare = isIssue || bare.has(script);
if (text === null && wasBare && script.startsWith('ct/')) {
const vm = `vm/${script.slice(3)}`;
if ((text = await fetchScript(vm)) !== null) script = vm;
}
if (text === null) {
const where = isIssue ? 'on the default branch' : 'in this PR';
notBuilt.push(wasBare
? (script.startsWith('ct/') ? `${code(script.slice(3, -3))}: neither \`ct/\` nor \`vm/\` has it ${where}` : `${code(script)} does not exist ${where}`)
: `\`${script}\` does not exist in this PR (an \`install/\` script is built through the \`ct/\` script of the same name)`);
continue;
}
const isVm = script.startsWith('vm/');
// The test hosts are amd64; an ARM-only VM script refuses to run there.
if (isVm && /^\s*vm_require_arch\s+arm64\b/m.test(text)) {
notBuilt.push(`${code(script)}: ARM64 only, the test hosts are amd64`);
continue;
}
// Scripts that ask for the OS get built once per OS.
const oses = !isVm && /msg_menu\s+"Choose the container OS"/.test(text) ? ['debian', 'alpine'] : [''];
const name = isVm ? `vm-${script.slice(3, -3)}` : script.slice(3, -3);
// Every ct/ script on every platform: the same files run on Proxmox
// and, through community-scripts/core's Incus backend, on Incus.
// VMs are Proxmox only. ARM (Incus on GitHub's arm64 runner) is
// information only and never decides the result; a script that
// says var_arm64=no is not built there. Unset is built with
// var_arm64=yes, to see whether it would work.
const arm = (text.match(/^[ \t]*var_arm64="\$\{var_arm64:-(yes|no)\}"/m) || [])[1] || 'unset';
let platforms = isVm ? ['pve'] : arm === 'no' ? ['pve', 'incus'] : ['pve', 'incus', 'arm'];
if (!isVm && arm === 'no' && (!only.size || only.has('arm'))) notBuilt.push(`${code(script)} on ARM64: the script sets \`var_arm64=no\``);
if (only.size) {
if (isVm && !only.has('pve')) notBuilt.push(`${code(script)}: VM scripts are built on \`pve\` only`);
platforms = platforms.filter(p => only.has(p));
}
for (const platform of platforms) {
for (const os of oses) include.push({ script, os, platform, arm64: platform === 'arm' ? arm : '', name: `${os ? `${name}-${os}` : name}-${platform}` });
}
}
for (const script of entries.slice(MAX)) notBuilt.push(`\`${script}\`: at most ${MAX} scripts per run, name it with \`/test ${script}\``);
if (include.length === 0) {
await sticky([
MARK, '### Proxmox script test', '',
isIssue || onlyList.length
? `ℹ️ Nothing to build${onlyList.length ? ` on ${onlyList.join(', ')}` : ''}:`
: 'ℹ️ Nothing to build: this test builds `ct/` and `vm/` scripts (and changed `install/` scripts through their `ct/` script).', '',
...(notBuilt.length ? notBuilt.map(n => `- ${n}`) : ['- this PR changes no `ct/`, `vm/` or `install/` script']),
].join('\n'));
core.notice('No ct/ or vm/ script to build');
return;
}
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
// A check run belongs to a PR's commit; an issue has none.
let checkId = '';
if (!isIssue) {
const { data: check } = await github.rest.checks.create({
// A partial run gets its own name: it must not stand in for the
// full test, e.g. as a required check.
owner, repo, name: `Proxmox script test${onlyList.length ? ` (${onlyList.join(', ')} only)` : ''}`, head_sha: headSha, status: 'in_progress',
details_url: runUrl,
output: { title: `Building ${include.length}`, summary: include.map(i => `- \`${i.script}\` ${[i.platform, i.os].filter(Boolean).join(' · ')}`).join('\n') },
});
checkId = String(check.id);
}
core.setOutput('matrix', JSON.stringify(include));
core.setOutput('pr', String(prNumber));
core.setOutput('repo', head.full_name);
core.setOutput('sha', headSha);
core.setOutput('check_id', checkId);
core.setOutput('not_built', JSON.stringify(notBuilt));
core.setOutput('platforms', onlyList.join(' '));
// Who asked: the /test commenter, or whoever dispatched the run.
core.setOutput('actor', context.actor);
await sticky([
MARK, '### Proxmox script test', '',
`⏳ Building ${include.length} at ${headSha.slice(0, 7)}${isIssue ? ' (default branch)' : ''}${onlyList.length ? `, only on ${onlyList.join(', ')}` : ''} — [watch it live](${runUrl}) (open the \`test\` job)`, '',
...include.map(i => `- \`${i.script}\` (${[i.platform, i.os].filter(Boolean).join(' · ')})`),
...(notBuilt.length ? ['', '**Not built:**', ...notBuilt.map(n => `- ${n}`)] : []),
].join('\n'));
test:
needs: gate
if: needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
# Only `proxmox-test` / `incus-test`, never `self-hosted`: the runners are
# registered without default labels so that no other workflow can be
# scheduled onto them. ARM runs on GitHub's own arm64 runner, a fresh VM
# per job (free for public repositories).
runs-on: ${{ matrix.platform == 'arm' && 'ubuntu-24.04-arm' || matrix.platform == 'incus' && 'incus-test' || 'proxmox-test' }}
# ARM is information only: a failed or missing ARM job (no runner, no
# minutes left, ...) never turns the run red. The report leaves it out of
# the result as well.
continue-on-error: ${{ matrix.platform == 'arm' }}
permissions:
contents: read
timeout-minutes: 90 # install (45) + update (30) + checks
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.gate.outputs.matrix) }}
name: test (${{ matrix.name }})
steps:
# The default branch, never the PR: the harness itself must be trusted.
- uses: actions/checkout@v5
with:
sparse-checkout: .github/workflows/scripts
persist-credentials: false
# The arm64 runner is a plain Ubuntu VM: Incus and the screenshot
# browser are set up on it for this one job.
- name: Set up the ARM runner
if: matrix.platform == 'arm'
timeout-minutes: 20 # about 2 when healthy; a hang must not hold the report
run: sudo bash .github/workflows/scripts/arm-runner-setup.sh
- name: Build and check ${{ matrix.script }}
env:
PR_REPO: ${{ needs.gate.outputs.repo }}
PR_SHA: ${{ needs.gate.outputs.sha }}
SCRIPT: ${{ matrix.script }}
OS: ${{ matrix.os }}
# ARM is the Incus harness on another machine.
PLATFORM: ${{ matrix.platform == 'arm' && 'incus' || matrix.platform }}
VAR_ARM64: ${{ matrix.platform == 'arm' && 'yes' || '' }}
UPSTREAM: ${{ github.repository }}
CTID_BASE: ${{ github.repository == 'community-scripts/ProxmoxVE' && '9050' || '9000' }}
# Only so release lookups are not rate limited: unauthenticated, all
# test containers share the lab's one public IP and 60 calls an hour.
# This job's token can read public contents and expires with the job.
GH_API_TOKEN: ${{ github.token }}
OUT_DIR: ${{ runner.temp }}/result
# The self-hosted runners run as root; the GitHub one needs sudo (pct
# and incus do).
run: >-
${{ matrix.platform == 'arm' && 'sudo --preserve-env=PR_REPO,PR_SHA,SCRIPT,OS,PLATFORM,VAR_ARM64,UPSTREAM,CTID_BASE,GH_API_TOKEN,OUT_DIR,GITHUB_RUN_ID,GITHUB_RUN_ATTEMPT' || '' }}
bash .github/workflows/scripts/pve-run-script.sh
- if: always() && matrix.platform == 'arm'
run: sudo chmod -R a+rX "$RUNNER_TEMP/result" 2>/dev/null || true
- if: always()
uses: actions/upload-artifact@v4
with:
name: result-${{ matrix.name }}
path: ${{ runner.temp }}/result
if-no-files-found: ignore
retention-days: 14
report:
needs: [gate, test]
if: always() && needs.gate.result == 'success' && needs.gate.outputs.matrix != '[]' && needs.gate.outputs.matrix != ''
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
contents: write # the ci-screenshots branch only; main is protected
issues: write
pull-requests: write
steps:
- uses: actions/download-artifact@v5
with:
pattern: result-*
path: results
- uses: actions/github-script@v8
env:
MATRIX: ${{ needs.gate.outputs.matrix }}
PR: ${{ needs.gate.outputs.pr }}
SHA: ${{ needs.gate.outputs.sha }}
CHECK_ID: ${{ needs.gate.outputs.check_id }}
TEST_RESULT: ${{ needs.test.result }}
ACTOR: ${{ needs.gate.outputs.actor }}
NOT_BUILT: ${{ needs.gate.outputs.not_built }}
TRUSTED: ${{ needs.gate.outputs.trusted }}
KIND: ${{ needs.gate.outputs.kind }}
PLATFORMS: ${{ needs.gate.outputs.platforms }}
with:
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const matrix = JSON.parse(process.env.MATRIX);
// download-artifact unpacks a single artifact straight into results/
// and several into results/<name>/, so look for every result.json
// and match it by what it says it is.
const path = require('path');
const found = new Map();
const walk = dir => {
if (!fs.existsSync(dir)) return;
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.name === 'result.json') {
try {
const r = JSON.parse(fs.readFileSync(p, 'utf8'));
// Written on the host that ran the PR's code: anything but a
// known status is an error, never something new.
if (!['pass', 'fail', 'unsupported', 'error'].includes(r.status)) r.status = 'error';
const log = path.join(dir, 'install.log');
r.install_log = fs.existsSync(log) ? fs.readFileSync(log, 'utf8') : '';
const shot = path.join(dir, 'screenshot.png');
r.shot_file = fs.existsSync(shot) ? shot : '';
if (!['updated', 'no-update', 'fail', 'error', ''].includes(r.update_status || '')) r.update_status = '';
// Installed fine, but the update could not be tested (e.g. a
// GitHub rate limit): not a pass, and not the PR's fault.
if (r.status === 'pass' && r.update_status === 'error') { r.status = 'error'; r.step = `update ${r.update_step}`; }
const ulog = path.join(dir, 'update.log');
r.update_log = fs.existsSync(ulog) ? fs.readFileSync(ulog, 'utf8') : '';
const ushot = path.join(dir, 'screenshot-update.png');
r.shot_update_file = fs.existsSync(ushot) ? ushot : '';
// ARM runs the Incus harness on an arm64 machine; result.json
// says so in arch. Only the platforms this workflow knows: an
// unknown one would never match a matrix row anyway.
if (r.arch === 'aarch64') r.platform = 'arm';
else if (!['pve', 'incus'].includes(r.platform)) r.platform = 'pve';
found.set(`${r.script}|${r.platform}|${r.os}`, r);
} catch (err) { core.warning(`unreadable ${p}: ${err.message}`); }
}
}
};
walk('results');
// A job that died before writing result.json still gets a row.
const rows = matrix.map(m => ({ ...(found.get(`${m.script}|${m.platform || 'pve'}|${m.os || 'default'}`) || {
script: m.script, os: m.os || 'default', platform: m.platform || 'pve',
status: process.env.TEST_RESULT === 'cancelled' ? 'cancelled' : 'error',
step: 'no result (runner or job failure)', duration: 0, log_tail: '', install_log: '', shot_file: '',
update_status: '', update_log: '', shot_update_file: '',
}), name: m.name }));
// Where a row ran: "pve", "incus · alpine", "arm64", or "VM" (Proxmox only).
const isVm = r => r.script.startsWith('vm/');
const isArm = r => r.platform === 'arm';
const where = r => {
if (isVm(r)) return 'VM';
const p = isArm(r) ? 'arm64' : r.platform;
return r.os && r.os !== 'default' ? `${p} · ${r.os}` : p;
};
// ARM is information only: shown, never counted. The result, the
// check, the review and the approval come from Proxmox and Incus.
const coreRows = rows.filter(r => !isArm(r));
const armRows = rows.filter(isArm);
// Screenshots go to an orphan branch, because a comment can only show
// an image that has a URL. One file per PR and script, overwritten on
// every run; the link names the commit, so no cache shows an old one.
// The PNG comes from the host that ran the PR's code: check it again.
const BRANCH = 'ci-screenshots';
const prNum = Number(process.env.PR);
const isPng = buf => buf.length > 8 && buf.length < 3000000 &&
buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
const ensureBranch = async () => {
try { await github.rest.git.getRef({ owner, repo, ref: `heads/${BRANCH}` }); return; }
catch (e) { if (e.status !== 404) throw e; }
const { data: blob } = await github.rest.git.createBlob({ owner, repo, encoding: 'utf-8',
content: 'Screenshots taken by pr-script-test.yml for the PR comments. Not code; never merge this branch.\n' });
const { data: tree } = await github.rest.git.createTree({ owner, repo, tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: blob.sha }] });
const { data: commit } = await github.rest.git.createCommit({ owner, repo, message: 'Start ci-screenshots', tree: tree.sha, parents: [] });
await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${BRANCH}`, sha: commit.sha });
};
const uploadShot = async (r, local, suffix) => {
const buf = fs.readFileSync(local);
if (!isPng(buf)) { core.warning(`${r.script}: screenshot is not a plausible PNG, skipped`); return ''; }
const file = `${process.env.KIND === 'issue' ? 'issue' : 'pr'}-${prNum}/${r.name}${suffix}.png`;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await ensureBranch();
let sha;
try { sha = (await github.rest.repos.getContent({ owner, repo, path: file, ref: BRANCH })).data.sha; }
catch (e) { if (e.status !== 404) throw e; }
const { data } = await github.rest.repos.createOrUpdateFileContents({
owner, repo, branch: BRANCH, path: file, sha, content: buf.toString('base64'),
message: `Screenshot: ${r.script} (${where(r)})${suffix} for #${prNum} at ${process.env.SHA.slice(0, 7)}`,
});
return `https://raw.githubusercontent.com/${owner}/${repo}/${data.commit.sha}/${file}`;
} catch (e) {
// Two reports at once race on the branch head; try again.
core.warning(`${r.script}: screenshot upload attempt ${attempt} failed: ${e.message}`);
await new Promise(res => setTimeout(res, 2000 * attempt));
}
}
return '';
};
for (const r of rows) {
if (r.shot_file) r.shot_url = await uploadShot(r, r.shot_file, '');
if (r.shot_update_file) r.shot_update_url = await uploadShot(r, r.shot_update_file, '-after-update');
}
// unsupported: the script needs a CPU feature the test host lacks
// (AVX2 and the like). Not the PR's fault, so not a failure.
const icon = { pass: '✅', fail: '❌', error: '⚠️', cancelled: '⏹️', unsupported: '🖥️' };
const dur = s => `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`;
// Text from the build lands outside code blocks here: no table breaks,
// no code spans, and no @mention of a stranger (zero-width space).
const clean = s => String(s || '').replace(/[|\r\n]/g, ' ').replace(/`/g, "'").replace(/@/g, '@​').slice(0, 200);
const fence = s => String(s || '').replace(/~~~~/g, '~~~');
// fail: the app broke, the PR's business. error/cancelled: our test
// setup did, not the PR's. unsupported: the host's CPU is too old.
const count = s => coreRows.filter(r => r.status === s).length;
const passed = count('pass');
const untestable = count('unsupported');
const broken = count('fail');
const setupTrouble = count('error') + count('cancelled');
const allPass = passed === coreRows.length;
// `/test arm`: nothing that counts was built, so the result is
// information only as a whole.
const armOnly = coreRows.length === 0;
// Built on only some platforms (`/test pve`): a pass there says
// nothing about the others, so it neither approves nor lifts an
// earlier "changes requested". A failure is still a failure.
const partial = !!process.env.PLATFORMS;
const cancelled = (armOnly ? rows : coreRows).every(r => r.status === 'cancelled');
const of = `**${passed}/${coreRows.length} passed**`;
const armPassed = armRows.filter(r => r.status === 'pass').length;
const armLine = armRows.length && !armOnly
? `ARM64 (information only, not counted): ${armPassed}/${armRows.length} passed${armPassed < armRows.length ? `; failures there do not block this PR` : ''}.`
: '';
const headline = armOnly
? `ℹ️ ARM64 only, information only: **${armPassed}/${armRows.length} passed**`
: broken
? `❌ ${of}`
: setupTrouble
? `⚠️ ${of}, ${setupTrouble} could not be tested because of a problem with the test setup (not the PR)`
: untestable
? `🖥️ ${of}, ${untestable} need${untestable === 1 ? 's' : ''} a newer CPU than the test host has (nothing failed)`
: `✅ ${of}`;
// Update: the installed app is set back a version and updated with
// the PR's update_script. ➖ = it ran but had nothing to update.
const updateIcon = { updated: '✅', 'no-update': '➖', fail: '❌', error: '⚠️' };
const detail = r => {
if (r.status !== 'pass') return clean(r.step);
const ip = /^[0-9.]{7,15}$/.test(r.ip || '') ? r.ip : '';
if (isVm(r) && !r.url) return ip ? `booted, got ${ip}` : 'booted, no DHCP lease (an installer ISO, or no network)';
const reach = r.url ? `${clean(r.url)} → ${r.http_code}` : 'no URL to check';
return r.update_status === 'no-update' ? `${reach}; update: nothing to update${r.update_step ? ` (${clean(r.update_step)})` : ''}` : reach;
};
const table = [
'| Script | Platform | Install | Update | Duration | Detail |',
'|---|---|---|---|---|---|',
...rows.map(r => `| \`${r.script}\` | ${where(r)} | ${r.update_status ? '✅' : icon[r.status] || '❔'} | ${updateIcon[r.update_status] || '–'} | ${dur(r.duration || 0)} | ${detail(r)} |`),
].join('\n');
// A comment holds 65536 characters. Each script gets an equal share
// and a log that does not fit loses its beginning: the end is where
// a failure shows.
const budget = Math.floor(52000 / rows.length);
const tail = (text, max) => {
text = fence(text).trimEnd();
if (text.length <= max) return text;
const kept = text.slice(text.length - max);
const cut = text.slice(0, text.length - max).split('\n').length;
return `… ${cut} earlier lines cut, the full log is in the run artifacts\n${kept.slice(kept.indexOf('\n') + 1)}`;
};
const details = (summaryLine, text, max) => [
`<details><summary>${summaryLine}</summary>`, '',
'~~~~text', tail(text, max), '~~~~', '</details>',
].join('\n');
const logs = rows.map(r => {
const parts = [];
const failed = r.status !== 'pass';
// A VM script's own output is all there is: no install log inside.
const buildShare = (failed || isVm(r)) && r.log_tail ? Math.min(r.log_tail.length, Math.floor(budget / 3)) : 0;
if (buildShare) parts.push(details(`${icon[r.status] || '❔'} ${r.script} (${where(r)}) — build log`, r.log_tail, buildShare));
if (isVm(r)) return parts.join('\n\n');
// The update log gets up to a third of what is left, the install log the rest.
const updateShare = r.update_log ? Math.min(r.update_log.length, Math.floor((budget - buildShare) / 3)) : 0;
if (r.install_log) {
const lines = r.install_log.trimEnd().split('\n').length;
parts.push(details(`📄 ${r.script} (${where(r)}) — install log (${lines} lines)`, r.install_log, budget - buildShare - updateShare - 400));
} else if (!buildShare) {
parts.push(`<sub>${r.script} (${where(r)}): no install log was captured</sub>`);
}
if (updateShare) {
const lines = r.update_log.trimEnd().split('\n').length;
parts.push(details(`🔄 ${r.script} (${where(r)}) — update log (${lines} lines)`, r.update_log, updateShare));
}
return parts.join('\n\n');
});
const shots = rows.filter(r => r.shot_url || r.shot_update_url).map(r => [
`<details open><summary>📸 ${r.script} (${where(r)}) — ${r.url ? clean(r.url) : 'console'}${r.shot_url && r.shot_update_url ? ': after install / after update' : r.shot_update_url ? ': after update' : ''}</summary>`, '',
[r.shot_url, r.shot_update_url].filter(Boolean)
.map((u, i, all) => `<img src="${u}" width="${all.length > 1 ? 420 : 640}" alt="Screenshot of ${r.script}">`).join(' '),
'', '</details>',
].join('\n'));
const summary = [
`${headline} at \`${process.env.SHA.slice(0, 7)}\``, '',
table, '',
...(armLine ? [`<sub>${armLine}</sub>`, ''] : []),
...(partial ? [`<sub>Built only on ${process.env.PLATFORMS.split(' ').join(', ')}; comment \`/test\` for every platform.</sub>`, ''] : []),
...shots, '',
...(() => { try { const n = JSON.parse(process.env.NOT_BUILT || '[]'); return n.length ? ['**Not built:**', ...n.map(x => `- ${x}`), ''] : []; } catch (e) { return []; } })(),
...logs, '',
`Full logs: [workflow run](${runUrl}) (artifacts \`result-*\`). Comment \`/test${partial ? ` ${process.env.PLATFORMS}` : ''}${process.env.KIND === 'issue' ? ' ' + [...new Set(rows.map(r => r.script.slice(3, -3)))].join(' ') : ''}\` to build again.`,
].join('\n');
// Issues have no check run (there is no PR commit to attach it to).
if (process.env.CHECK_ID) {
await github.rest.checks.update({
owner, repo, check_run_id: Number(process.env.CHECK_ID), status: 'completed',
// Untestable here is "test it by hand", not a pass: action_required,
// because a required check would count neutral as passed.
conclusion: cancelled ? 'cancelled' : armOnly ? 'neutral' : allPass ? 'success' : broken ? 'failure' : 'action_required',
output: {
title: armOnly
? `ARM64 ${armPassed}/${armRows.length} passed (information only)`
: `${passed}/${coreRows.length} passed${armRows.length ? ` (+ ARM ${armPassed}/${armRows.length}, not counted)` : ''}`,
summary: summary.slice(0, 65000),
},
});
}
// The result pings whoever started the test. A mention added by
// editing a comment does not reliably notify, so the old comment
// goes and the result is posted as a new one, at the end of the
// thread where it is looked for.
// Whoever started the test and the PR's author, once each; no bots.
const pr = Number(process.env.PR);
const isIssue = process.env.KIND === 'issue';
const { data: prInfo } = isIssue
? await github.rest.issues.get({ owner, repo, issue_number: pr })
: await github.rest.pulls.get({ owner, repo, pull_number: pr });
const author = prInfo.user && prInfo.user.type === 'User' ? prInfo.user.login : '';
const login = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})$/;
const ping = [...new Set([process.env.ACTOR || '', author])].filter(l => login.test(l)).map(l => `@${l} `).join('');
const MARK = '<!-- pr-script-test -->';
const body = `${MARK}\n### Proxmox script test\n\n${ping}${summary}`.slice(0, 65000);
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr });
for (const c of comments.filter(c => c.body.includes(MARK) && c.user.type === 'Bot')) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id });
}
const { data: posted } = await github.rest.issues.createComment({ owner, repo, issue_number: pr, body });
// An issue has nothing to review or approve: the comment is all.
if (isIssue) return;
// A real failure requests changes on the tested commit. Untestable
// (CPU), runner errors and cancelled runs are not the PR's fault and
// leave the review state alone. Our previous "changes requested" is
// always dismissed first: a pass lifts it, a new failure replaces it.
const REVIEW_MARK = '<!-- pr-script-test-review -->';
const failedRows = cancelled ? [] : coreRows.filter(r => r.status === 'fail');
const sha7 = process.env.SHA.slice(0, 7);
// Trusted PRs (scripts moving over from DevScripts) are approved when
// every script passes -- not on untestable or setup trouble, which
// means "not tested", not "good". That approval counts toward the
// ruleset's required review; a push dismisses it as stale, and a
// later run that does not fully pass withdraws it.
// Anyone with write access can push to a migration branch, and the
// approval counts toward the required review. So it only covers
// what a migration contains: ct/ and install/ scripts. A PR that
// also changes anything else (workflows, misc/, ...) is tested but
// never approved.
let approve = process.env.TRUSTED === 'true' && allPass && !partial;
// A partial run that passed leaves every review as it is.
const leaveReviews = partial && !failedRows.length;
let noApproveWhy = '';
if (approve) {
const changed = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr });
const other = changed.map(f => f.filename).filter(n => !/^(ct|install)\/[A-Za-z0-9._-]+\.sh$/.test(n));
if (other.length) {
approve = false;
noApproveWhy = `Not approved automatically: besides \`ct/\` and \`install/\` scripts this PR changes ${other.slice(0, 5).map(n => '`' + n.replace(/[`\r\n]/g, '') + '`').join(', ')}${other.length > 5 ? ', …' : ''}. That needs a human review.`;
}
}
if (noApproveWhy) {
await github.rest.issues.updateComment({ owner, repo, comment_id: posted.id, body: `${body}\n\n> ${noApproveWhy}`.slice(0, 65000) });
}
try {
const reviews = await github.paginate(github.rest.pulls.listReviews, { owner, repo, pull_number: pr });
const ours = state => reviews.filter(v => v.user && v.user.type === 'Bot' && v.state === state && (v.body || '').includes(REVIEW_MARK));
for (const v of leaveReviews ? [] : ours('CHANGES_REQUESTED')) {
await github.rest.pulls.dismissReview({
owner, repo, pull_number: pr, review_id: v.id,
message: failedRows.length ? 'Replaced by a newer Proxmox script test.' : `Proxmox script test passed at ${sha7}.`,
});
}
if (!approve && !cancelled && !leaveReviews) {
for (const v of ours('APPROVED')) {
await github.rest.pulls.dismissReview({
owner, repo, pull_number: pr, review_id: v.id,
message: `Withdrawn: the Proxmox script test at ${sha7} did not fully pass.`,
});
}
}
if (approve && !ours('APPROVED').some(v => v.commit_id === process.env.SHA)) {
await github.rest.pulls.createReview({
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'APPROVE',
body: [
REVIEW_MARK, `**Proxmox script test passed** at \`${sha7}\`: ${rows.map(r => `\`${r.script}\``).join(', ')}.`, '',
`Script moved over from DevScripts, built and checked on a real Proxmox host. [Test result](${posted.html_url}).`,
].join('\n'),
});
}
if (failedRows.length) {
await github.rest.pulls.createReview({
owner, repo, pull_number: pr, commit_id: process.env.SHA, event: 'REQUEST_CHANGES',
body: [
REVIEW_MARK, `**Proxmox script test failed** at \`${sha7}\`:`, '',
...failedRows.map(r => `- \`${r.script}\` (${where(r)}): ${clean(r.step)}`), '',
`Logs and details: [test result](${posted.html_url}). Push a fix, then ask for \`/test\` again.`,
].join('\n'),
});
}
} catch (e) {
core.warning(`Could not update the review state: ${e.message}`);
}
+141
View File
@@ -0,0 +1,141 @@
name: Request Script Eligibility Check
# category.slug is the same string GitHub uses in the "new discussion" URL
# (?category=request-script, linked from every repo's issue chooser), so this
# guard matches the live category without needing its numeric id.
on:
discussion:
types: [created, edited, reopened]
jobs:
check-eligibility:
if: github.repository == 'community-scripts/ProxmoxVE' && github.event.discussion.category.slug == 'request-script'
runs-on: ubuntu-latest
permissions:
discussions: write
issues: write
contents: read
steps:
- name: Verify stars/age, flag or close
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const discussion = context.payload.discussion;
if (!discussion || discussion.state === "closed") {
core.info("Discussion missing or already closed — skipping.");
return;
}
const body = discussion.body || "";
const lines = body.split("\n");
const fields = {};
let current = null;
for (const line of lines) {
const m = line.match(/^###\s+(.+?)\s*$/);
if (m) {
current = m[1].trim();
fields[current] = [];
} else if (current) {
fields[current].push(line);
}
}
function fieldText(label) {
const raw = (fields[label] || []).join("\n").trim();
return raw === "_No response_" ? "" : raw;
}
const sourceRepo = fieldText("Source Repository");
const justification = fieldText("Justification for an exception");
let eligible = false;
let verified = false;
const ghMatch = sourceRepo.match(/^https?:\/\/(?:www\.)?github\.com\/([^\/\s]+)\/([^\/\s#?]+?)(?:\.git)?\/?(?:[#?].*)?$/i);
if (ghMatch) {
const [, repoOwner, repoName] = ghMatch;
try {
const { data: repoData } = await github.rest.repos.get({ owner: repoOwner, repo: repoName });
const stars = repoData.stargazers_count;
const createdAt = new Date(repoData.created_at);
const now = new Date();
const ageMonths =
(now.getFullYear() - createdAt.getFullYear()) * 12 +
(now.getMonth() - createdAt.getMonth()) -
(now.getDate() < createdAt.getDate() ? 1 : 0);
eligible = stars >= 1000 && ageMonths >= 6;
verified = true;
core.info(`Verified ${repoOwner}/${repoName}: ${stars} stars, ${ageMonths} months old, eligible=${eligible}`);
} catch (err) {
core.warning(`Could not verify ${repoOwner}/${repoName}: ${err.message} — treating as unverifiable.`);
}
} else {
core.info("Source Repository is not a github.com URL — cannot auto-verify, treating as unverifiable.");
}
if (eligible) {
core.info(`Discussion #${discussion.number} meets the requirements — no action.`);
return;
}
const labels = (discussion.labels || []).map(l => l.name);
const reasonLine = verified
? "the source repository does not meet the 1,000-star / 6-month minimum adoption requirement"
: "the source repository's star count and age could not be verified automatically";
if (justification) {
if (labels.includes("investigation")) {
core.info(`Discussion #${discussion.number} already flagged for investigation — skipping.`);
return;
}
try {
await github.rest.issues.createLabel({
owner, repo, name: "investigation",
color: "fbca04",
description: "Below the normal script-request threshold; a justification was given and needs maintainer review.",
});
} catch (err) {
if (err.status !== 422) throw err;
}
const labelQuery = await github.graphql(
`query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { label(name: "investigation") { id } } }`,
{ owner, repo }
);
const labelNodeId = labelQuery.repository.label.id;
await github.graphql(
`mutation($labelableId: ID!, $labelIds: [ID!]!) { addLabelsToLabelable(input: { labelableId: $labelableId, labelIds: $labelIds }) { clientMutationId } }`,
{ labelableId: discussion.node_id, labelIds: [labelNodeId] }
);
await github.graphql(
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
{
discussionId: discussion.node_id,
body:
`Thanks for the justification — ${reasonLine}, so this has been flagged \`investigation\` for a maintainer ` +
`to review manually rather than closed automatically.`,
}
);
} else {
await github.graphql(
`mutation($discussionId: ID!, $body: String!) { addDiscussionComment(input: { discussionId: $discussionId, body: $body }) { comment { id } } }`,
{
discussionId: discussion.node_id,
body:
`This request doesn't meet the minimum requirements (1,000 stars, project at least 6 months old) — ` +
`${reasonLine}, and no justification was given. Closing automatically.\n\n` +
`If you believe this project qualifies for an exception, edit this discussion to fill in ` +
`**Justification for an exception** and reopen it — it will then be flagged for manual review instead of closed again.`,
}
);
await github.graphql(
`mutation($discussionId: ID!) { closeDiscussion(input: { discussionId: $discussionId, reason: OUTDATED }) { discussion { id } } }`,
{ discussionId: discussion.node_id }
);
}
+192
View File
@@ -0,0 +1,192 @@
#!/usr/bin/env bash
# Turns a GitHub-hosted arm64 runner (ubuntu-24.04-arm) into a throwaway Incus
# test host for pve-run-script.sh (PLATFORM=incus): the same layout as
# incus-ci-host-setup.sh, minus what a VM that lives for one job does not need
# (services, sweeper, runner registration). Run with sudo.
#
# The runner is a fresh VM per job and is discarded afterwards; nothing here
# has to be undone.
set -euo pipefail
NET_ADDR=10.98.0.1
NET_CIDR=10.98.0.0/24
SERVE_ROOT=/srv/ci
SERVE_PORT=18080
start=$(date +%s)
# --- Incus ----------------------------------------------------------------------
# Ubuntu 24.04's own incus is 6.0.0, which lacks `-f csv,noheader`; the engine
# then finds no storage pool. Zabbly (the Incus maintainers' packages) has the
# current 6.0 LTS, as Debian 13 does.
mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.zabbly.com/key.asc -o /etc/apt/keyrings/zabbly.asc
# The key Zabbly publishes in its install instructions, not whatever the
# server returns today.
gpg --show-keys --with-colons /etc/apt/keyrings/zabbly.asc 2>/dev/null |
awk -F: '/^fpr/ {print $10; exit}' | grep -qx 4EFC590696CB15B87C73A3AD82CC8797C838DCFD ||
{ echo "::error::unexpected Zabbly signing key"; exit 1; }
# Only the Incus packages may come from there; everything else stays Ubuntu's.
printf '%s\n' 'Package: *' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 1' '' \
'Package: incus* liblxc* lxcfs* qemu* edk2*' 'Pin: origin pkgs.zabbly.com' 'Pin-Priority: 600' \
>/etc/apt/preferences.d/zabbly
printf '%s\n' 'Enabled: yes' 'Types: deb' 'URIs: https://pkgs.zabbly.com/incus/lts-6.0' \
"Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")" 'Components: main' \
"Architectures: $(dpkg --print-architecture)" 'Signed-By: /etc/apt/keyrings/zabbly.asc' \
>/etc/apt/sources.list.d/zabbly-incus-lts-6.0.sources
# Each slow step has its own limit, so a hang names itself in the log instead
# of holding the PR report until the job times out.
step() { local t=$1; shift; timeout "$t" "$@" || { echo "::error::'$*' failed or hung (limit ${t})"; return 1; }; }
# What the runner's network looks like when apt can't get through.
netdiag() {
echo "::group::Network on this runner"
resolvectl status 2>&1 | grep -E 'DNS Servers|Current DNS|resolv.conf mode' || cat /etc/resolv.conf
local h
for h in ports.ubuntu.com pkgs.zabbly.com github.com; do
echo "$h -> $(timeout 10 getent ahostsv4 "$h" | awk '{print $1; exit}')"
timeout 15 curl -s -o /dev/null -w " http %{http_code} in %{time_total}s\n" "http://$h/" || echo " no answer"
done
echo "::endgroup::"
}
# The runner's resolver (Azure's 168.63.129.16) stopped resolving the arm64
# mirror on 2026-11-11 while every other name worked. Then the name is
# looked up over DNS-over-HTTPS and pinned in /etc/hosts for this job.
for h in ports.ubuntu.com; do
timeout 10 getent ahostsv4 "$h" >/dev/null && continue
ip=$(curl -fsS --max-time 10 -H 'accept: application/dns-json' "https://1.1.1.1/dns-query?name=$h&type=A" |
jq -r '[.Answer[]? | select(.type == 1) | .data][0] // empty')
if [[ "$ip" =~ ^[0-9]+(\.[0-9]+){3}$ ]]; then
echo "$ip $h" >>/etc/hosts
echo "::warning::the runner's DNS can't resolve $h; using $ip from 1.1.1.1"
fi
done
step 5m apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 update -qq || { netdiag; exit 1; }
DEBIAN_FRONTEND=noninteractive step 10m apt-get install -y -qq incus jq >/dev/null
# Docker on the runner sets the FORWARD policy to DROP, which cuts the Incus
# containers off from the internet.
iptables -P FORWARD ACCEPT
step 2m incus admin init --preseed <<EOF
networks:
- name: incusbr0
type: bridge
config:
ipv4.address: ${NET_ADDR}/24
ipv4.nat: "true"
ipv6.address: none
storage_pools:
- name: default
driver: dir
profiles:
- name: default
devices:
root:
path: /
pool: default
type: disk
eth0:
name: eth0
network: incusbr0
type: nic
EOF
# The PR tree server the harness expects (a service on the lab hosts).
mkdir -p "$SERVE_ROOT"
nohup python3 -m http.server "$SERVE_PORT" --bind "$NET_ADDR" --directory "$SERVE_ROOT" >/tmp/ci-serve.log 2>&1 &
echo "Incus $(incus --version) ready after $(($(date +%s) - start))s"
# --- Screenshots ----------------------------------------------------------------
# Optional: without them the build is still tested. The harness takes one only
# when `chromium` exists, the user ci-shot exists and the CI_SHOT chain keeps
# that user off private addresses (here: the cloud metadata service at
# 169.254.169.254 and the runner itself).
shots() {
useradd --system --create-home --shell /usr/sbin/nologin ci-shot
# Ubuntu's apt chromium is a snap wrapper; Playwright ships a plain arm64
# build. Started directly with --screenshot, that build crashes on this
# runner (sandbox or not), so `chromium` here is a stand-in that takes the
# harness's arguments and drives the browser through Playwright instead.
mkdir -p /opt/pw
(cd /opt/pw && step 5m npm install --silent --no-audit --no-fund playwright-core@1.49.1 >/dev/null) || return 1
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers DEBIAN_FRONTEND=noninteractive \
step 10m /opt/pw/node_modules/.bin/playwright-core install --with-deps chromium >/dev/null || return 1
chmod -R a+rX /opt/pw /opt/pw-browsers
cat >/opt/pw/shot.js <<'EOF'
// node shot.js <url> <out.png> <width> <height>
const { chromium } = require('/opt/pw/node_modules/playwright-core');
(async () => {
const [url, out, w, h] = process.argv.slice(2);
const browser = await chromium.launch({ chromiumSandbox: true });
try {
const page = await browser.newPage({ viewport: { width: Number(w), height: Number(h) }, ignoreHTTPSErrors: true });
await page.goto(url, { waitUntil: 'load', timeout: 30000 }).catch(() => {});
await page.waitForTimeout(5000);
await page.screenshot({ path: out });
} finally {
await browser.close();
}
})().catch(e => { console.error(e.message); process.exit(1); });
EOF
cat >/usr/local/bin/chromium <<'EOF'
#!/usr/bin/env bash
# Stand-in for `chromium --headless ... --screenshot=<file> --window-size=W,H <url>`
# on the ARM runner (see arm-runner-setup.sh). Other flags are ignored.
url="" out="" w=1280 h=800
for a in "$@"; do
case "$a" in
--version) echo "Chromium (Playwright $(node -p "require('/opt/pw/node_modules/playwright-core/package.json').version"))"; exit 0 ;;
--screenshot=*) out="${a#*=}" ;;
--window-size=*) s="${a#*=}"; w="${s%,*}"; h="${s#*,}" ;;
-*) ;;
*) url="$a" ;;
esac
done
[[ -n "$url" && -n "$out" ]] || { echo "usage: chromium --screenshot=<file> <url>" >&2; exit 2; }
PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers exec node /opt/pw/shot.js "$url" "$out" "$w" "$h"
EOF
chmod 755 /usr/local/bin/chromium
# Ubuntu 24.04 forbids unprivileged user namespaces through AppArmor, and
# Chromium's sandbox needs them. The sandbox stays on; the restriction goes,
# on a VM that exists for this one job.
sysctl -qw kernel.apparmor_restrict_unprivileged_userns=0 2>/dev/null || true
local uid fw net ns
uid=$(id -u ci-shot)
for fw in iptables ip6tables; do
$fw -w -N CI_SHOT
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
done
iptables -w -A CI_SHOT -d "$NET_ADDR" -j REJECT
iptables -w -A CI_SHOT -d "$NET_CIDR" -j ACCEPT
for ns in 127.0.0.53 $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /run/systemd/resolve/resolv.conf 2>/dev/null); do
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
done
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
iptables -w -A CI_SHOT -d "$net" -j REJECT
done
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
done
# The same call the harness makes, on a page that needs no network.
local dir
dir=$(mktemp -d)
chown ci-shot "$dir"
# HOME: runuser keeps root's, which ci-shot cannot write (the harness does the same).
runuser -u ci-shot -- env HOME="$dir" timeout 60 chromium --headless=new --disable-gpu --hide-scrollbars \
--no-first-run --window-size=320,200 --user-data-dir="$dir/profile" \
--screenshot="$dir/shot.png" 'data:text/html,<h1>ci</h1>' &>"$dir/chromium.log" || true
if [[ ! -s "$dir/shot.png" ]]; then
echo "Chromium could not take a test screenshot:"
head -n 15 "$dir/chromium.log"
echo "userns: $(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>&1)"
rm -rf -- "$dir"
return 1
fi
rm -rf -- "$dir"
echo "$(chromium --version) ready for screenshots"
}
shots || echo "::warning::no screenshots on this ARM run (the build is still tested)"
echo "Runner ready after $(($(date +%s) - start))s"
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Installs ci-vm-controller.sh on the hypervisor (prox1): the script, the
# token reader, a systemd unit per CI VM, and each VM's config. Run as root
# on prox1 from a directory that also holds ci-vm-controller.sh:
#
# scp ci-controller-setup.sh ci-vm-controller.sh root@prox1:/root/ci-controller/
# ssh root@prox1 'cd /root/ci-controller && bash ci-controller-setup.sh'
#
# It does not start anything: a VM goes into service with
# systemctl enable --now ci-vm-controller@<vmid>
# once it has a 'clean' snapshot without any runner registered (see
# docs/guides/pr-script-testing.md, "Single-use hosts").
#
# The token (/etc/ci-controller/token) is put there by hand: a fine-grained
# token for the two repositories with Administration: read and write only.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f ci-vm-controller.sh ]] || { echo "ci-vm-controller.sh missing next to this script" >&2; exit 1; }
command -v jq >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq >/dev/null
install -m 755 ci-vm-controller.sh /usr/local/sbin/ci-vm-controller.sh
install -d -m 700 /etc/ci-controller
cat >/usr/local/sbin/ci-gh-token <<'EOF'
#!/bin/sh
# Prints the token ci-vm-controller.sh uses (Administration: write on the CI
# repositories). Swap this for a GitHub App token later without touching
# the controller.
exec cat /etc/ci-controller/token
EOF
chmod 755 /usr/local/sbin/ci-gh-token
cat >/etc/systemd/system/ci-vm-controller@.service <<'EOF'
[Unit]
Description=Single-use CI runner host, VM %i
After=network-online.target pve-cluster.service pvedaemon.service
Wants=network-online.target
# A missing token is a setup error, not something to retry every 30 s.
ConditionPathExists=/etc/ci-controller/token
[Service]
ExecStart=/usr/local/sbin/ci-vm-controller.sh %i
Restart=always
RestartSec=30
# Stopping the controller leaves the VM as it is; the next start resets it,
# which also ends a job that may still be running there.
KillMode=mixed
[Install]
WantedBy=multi-user.target
EOF
# One repository per host: see ci-vm-controller.sh.
conf() { # conf <vmid> <ip> <name> <label> <repo>
cat >"/etc/ci-controller/$1.conf" <<EOF
VM_IP=$2
HOST_NAME=$3
LABEL=$4
REPOS=$5
EOF
}
conf 200 10.201.146.232 pve-ci proxmox-test community-scripts/DevScripts
conf 201 10.201.146.231 pve-ci-2 proxmox-test community-scripts/ProxmoxVE
conf 202 10.201.146.233 incus-ci incus-test community-scripts/DevScripts
conf 203 10.201.146.234 incus-ci-2 incus-test community-scripts/ProxmoxVE
systemctl daemon-reload
echo "Installed. Token: $([[ -s /etc/ci-controller/token ]] && echo present || echo 'MISSING (/etc/ci-controller/token)')"
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bash
# Keeps one CI VM (pve-ci, incus-ci, ...) as a single-use GitHub runner host.
# Runs on the hypervisor (prox1), which PR code never touches.
#
# Every cycle:
# 1. roll the VM back to its clean snapshot and boot it;
# 2. register a just-in-time runner on it (generate-jitconfig) for the
# host's repository. Such a runner takes exactly one job and is deleted
# by GitHub afterwards, so its credentials are worthless once that job
# is over;
# 3. (with several repositories in REPOS: as soon as one runner is busy,
# delete the others that are still idle, so no second job starts on a
# host that is about to be reset);
# 4. once every runner of this cycle is gone, start over at 1.
#
# PR code runs as root on the VM, so nothing the VM says is trusted: when a
# job is over is read from GitHub's API, never from the VM.
#
# Config: /etc/ci-controller/<vmid>.conf (or the environment), see below.
# Usage: ci-vm-controller.sh <vmid> run forever (the systemd unit)
# ONE_CYCLE=1 ci-vm-controller.sh <vmid> one job, then stop (testing)
set -euo pipefail
VMID=${1:?usage: ci-vm-controller.sh <vmid>}
CONF=${CONF:-/etc/ci-controller/$VMID.conf}
# shellcheck source=/dev/null
[[ -f "$CONF" ]] && . "$CONF"
: "${VM_IP:?VM_IP}" # address of the CI VM
: "${HOST_NAME:?HOST_NAME}" # runner name prefix, e.g. pve-ci
: "${LABEL:?LABEL}" # proxmox-test or incus-test
# One repository per host. Several would each have a runner waiting on the
# VM, and a job of one could read the idle runner's JIT config of the other
# and use it elsewhere before it is withdrawn: one repository's PR code
# faking the other's results. (Several are fine within one trust domain.)
: "${REPOS:?REPOS, e.g. community-scripts/DevScripts}"
EXTRA_LABELS=${EXTRA_LABELS:-} # space-separated, e.g. for a test
SNAPSHOT=${SNAPSHOT:-clean}
TOKEN_CMD=${TOKEN_CMD:-/usr/local/sbin/ci-gh-token} # prints a token with repo Administration: write
PVE=${PVE:-} # prefix for qm, e.g. "ssh root@prox1" when not run on it
GUEST_SSH=${GUEST_SSH:-"ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new root@$VM_IP"}
POLL=${POLL:-20}
MAX_BUSY_MIN=${MAX_BUSY_MIN:-180} # the test job's own limit is 90
MAX_IDLE_MIN=${MAX_IDLE_MIN:-720} # recycle an idle host twice a day
ONE_CYCLE=${ONE_CYCLE:-}
log() { echo "[$(date -u +%FT%TZ)] vm $VMID: $*"; }
qm() { if [[ -n "$PVE" ]]; then $PVE qm "$@"; else command qm "$@"; fi; }
# Not in a subshell, so the status and the cached token survive the call:
# api METHOD PATH [JSON] -> body in $API_BODY, HTTP status in $API_STATUS.
API_FILE=$(mktemp)
trap 'rm -f "$API_FILE"' EXIT
TOKEN="" TOKEN_AT=0
api() {
local method=$1 path=$2 data=${3:-}
# Cached for 30 minutes (an App installation token lives 60).
if [[ -z "$TOKEN" || $(($(date +%s) - TOKEN_AT)) -gt 1800 ]]; then
TOKEN=$($TOKEN_CMD) || { log "no GitHub token"; API_STATUS=000 API_BODY=""; return 1; }
TOKEN_AT=$(date +%s)
fi
API_STATUS=$(curl -sS -o "$API_FILE" -w '%{http_code}' -X "$method" \
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2022-11-28' ${data:+-d "$data"} "https://api.github.com$path") || API_STATUS=000
API_BODY=$(<"$API_FILE")
[[ "$API_STATUS" != 000 ]]
}
reset_vm() {
log "rolling back to '$SNAPSHOT'"
[[ "$(qm status "$VMID")" == *running* ]] && qm stop "$VMID" --timeout 60 >/dev/null
qm rollback "$VMID" "$SNAPSHOT" >/dev/null
qm start "$VMID" >/dev/null
local i
for i in $(seq 1 60); do
$GUEST_SSH true 2>/dev/null && { log "booted"; return 0; }
sleep 5
done
log "no SSH after 5 minutes"
return 1
}
# Parallel arrays: one entry per runner registered in this cycle.
R_REPO=() R_ID=() R_GONE=()
register() { # register <owner/repo>
local repo=$1 name dir labels body cfg id
name="$HOST_NAME-${repo#*/}-$(date +%s)"
dir=/opt/actions-runner/${repo#*/}
labels=$(printf '%s\n' "$LABEL" $EXTRA_LABELS | jq -R . | jq -sc .)
api POST "/repos/$repo/actions/runners/generate-jitconfig" \
"$(jq -nc --arg n "$name" --argjson l "$labels" '{name: $n, runner_group_id: 1, labels: $l, work_folder: "_work"}')" || return 1
body=$API_BODY
[[ "$API_STATUS" == 201 ]] || { log "$repo: generate-jitconfig failed ($API_STATUS): $(jq -r .message <<<"$body" 2>/dev/null)"; return 1; }
id=$(jq -r .runner.id <<<"$body")
cfg=$(jq -r .encoded_jit_config <<<"$body")
# The config goes in through stdin, not the command line.
if ! $GUEST_SSH "umask 077; cat >/run/ci-jit-${repo#*/} && systemd-run --quiet --collect --unit=ci-jit-${repo#*/} \
-p WorkingDirectory=$dir -E RUNNER_ALLOW_RUNASROOT=1 \
/bin/sh -c 'cfg=\$(cat /run/ci-jit-${repo#*/}); rm -f /run/ci-jit-${repo#*/}; exec ./run.sh --jitconfig \"\$cfg\"'" <<<"$cfg"; then
log "$repo: could not start the runner on the VM"
api DELETE "/repos/$repo/actions/runners/$id" >/dev/null || true
return 1
fi
R_REPO+=("$repo") R_ID+=("$id") R_GONE+=(0)
log "$repo: runner $name (id $id) started"
}
drop() { # drop <index>: delete an idle runner; GitHub refuses a busy one
local i=$1
api DELETE "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" >/dev/null || return 1
if [[ "$API_STATUS" == 204 || "$API_STATUS" == 404 ]]; then R_GONE[$i]=1; return 0; fi
return 1
}
watch_cycle() {
local start busy_since="" i body status busy any_busy left offline_since=""
start=$(date +%s)
while :; do
sleep "$POLL"
any_busy=0 left=0 offline=0
for i in "${!R_ID[@]}"; do
[[ "${R_GONE[$i]}" == 1 ]] && continue
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || { left=1; continue; }
body=$API_BODY
if [[ "$API_STATUS" == 404 ]]; then
R_GONE[$i]=1
log "${R_REPO[$i]}: runner ${R_ID[$i]} is gone (job done or removed)"
continue
fi
left=1
busy=$(jq -r .busy <<<"$body"); status=$(jq -r .status <<<"$body")
[[ "$busy" == true ]] && any_busy=1
[[ "$status" == offline ]] && offline=1
done
if [[ "$any_busy" == 1 && -z "$busy_since" ]]; then
busy_since=$(date +%s)
log "a job started; withdrawing the idle runners"
fi
# A host that has started a job takes no other one: it will be reset.
if [[ -n "$busy_since" ]]; then
for i in "${!R_ID[@]}"; do
[[ "${R_GONE[$i]}" == 1 ]] && continue
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || continue
body=$API_BODY
[[ "$API_STATUS" == 200 && "$(jq -r .busy <<<"$body")" == false ]] && drop "$i" &&
log "${R_REPO[$i]}: idle runner ${R_ID[$i]} withdrawn"
done
fi
[[ "$left" == 0 ]] && { log "cycle over"; return 0; }
local now; now=$(date +%s)
if [[ -n "$busy_since" && $((now - busy_since)) -gt $((MAX_BUSY_MIN * 60)) ]]; then
log "job still running after $MAX_BUSY_MIN minutes; resetting anyway"
break
fi
if [[ -z "$busy_since" && $((now - start)) -gt $((MAX_IDLE_MIN * 60)) ]]; then
log "idle for $MAX_IDLE_MIN minutes; recycling"
break
fi
# A runner that never comes online (or dies while idle) holds the host
# forever otherwise.
if [[ -z "$busy_since" && "$offline" == 1 ]]; then
[[ -z "$offline_since" ]] && offline_since=$now
if [[ $((now - offline_since)) -gt 300 ]]; then log "a runner is offline for 5 minutes; resetting"; break; fi
else
offline_since=""
fi
done
for i in "${!R_ID[@]}"; do [[ "${R_GONE[$i]}" == 1 ]] || drop "$i" || true; done
}
# Registrations of this host that a stopped or crashed controller left behind:
# their VM was reset, so they are offline and never take a job, but they
# clutter the runner list. Busy or online ones are left alone.
cleanup_stale() {
local repo ids id
for repo in $REPOS; do
api GET "/repos/$repo/actions/runners?per_page=100" || continue
[[ "$API_STATUS" == 200 ]] || continue
ids=$(jq -r --arg p "$HOST_NAME-${repo#*/}-" \
'.runners[] | select((.name | startswith($p)) and .status == "offline" and (.busy | not)) | .id' <<<"$API_BODY")
for id in $ids; do
api DELETE "/repos/$repo/actions/runners/$id" && log "$repo: stale runner $id removed"
done
done
}
while :; do
R_REPO=() R_ID=() R_GONE=()
if reset_vm; then
cleanup_stale
ok=0
for repo in $REPOS; do register "$repo" && ok=1 || true; done
if [[ "$ok" == 1 ]]; then watch_cycle; else log "no runner could be registered"; sleep 60; fi
else
sleep 60
fi
[[ -n "$ONE_CYCLE" ]] && { log "ONE_CYCLE: stopping; the VM is left as the job left it"; break; }
done
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
# One-time setup of an Incus test host behind pr-script-test.yml: the same
# ct/ scripts as on Proxmox, built through community-scripts/core's Incus
# backend. Safe to re-run. Run as root on a disposable Debian 13 machine (it
# builds pull request code as root).
#
# incus-ci-host-setup.sh host only
# incus-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
# incus-ci-host-setup.sh <owner/repo> jit host + runner software only, for
# ci-vm-controller.sh (single-use host)
#
# The token is a runner *registration* token (valid one hour):
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
set -euo pipefail
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
SERVE_ROOT=/srv/ci
SERVE_PORT=18080
NET_ADDR=10.98.0.1
NET_CIDR=10.98.0.0/24
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq incus jq curl python3 iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
# Containers get a NATed bridge of their own (Incus runs DHCP and DNS on it)
# and a plain directory pool on the root disk.
if ! incus storage show default &>/dev/null; then
incus admin init --preseed <<EOF
networks:
- name: incusbr0
type: bridge
config:
ipv4.address: ${NET_ADDR}/24
ipv4.nat: "true"
ipv6.address: none
storage_pools:
- name: default
driver: dir
profiles:
- name: default
devices:
root:
path: /
pool: default
type: disk
eth0:
name: eth0
network: incusbr0
type: nic
EOF
fi
# Screenshots of the app under test are taken as this user, never as root.
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
# Same rules as on the Proxmox hosts: the screenshot browser may reach the
# test containers, DNS and the public internet, nothing private, and not this
# host's own address on the container network (the PR tree server).
cat >/usr/local/sbin/ci-shot-firewall <<EOF
#!/usr/bin/env bash
set -euo pipefail
uid=\$(id -u ci-shot)
for fw in iptables ip6tables; do
\$fw -w -N CI_SHOT 2>/dev/null || \$fw -w -F CI_SHOT
\$fw -w -C OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT 2>/dev/null ||
\$fw -w -I OUTPUT -m owner --uid-owner "\$uid" -j CI_SHOT
done
iptables -w -A CI_SHOT -d ${NET_ADDR} -j REJECT
iptables -w -A CI_SHOT -d ${NET_CIDR} -j ACCEPT
# systemd-resolved's stub; it forwards to the lab resolver itself.
iptables -w -A CI_SHOT -d 127.0.0.53 -p udp --dport 53 -j ACCEPT
iptables -w -A CI_SHOT -d 127.0.0.53 -p tcp --dport 53 -j ACCEPT
for ns in \$(awk '/^nameserver/ && \$2 !~ /:/ {print \$2}' /etc/resolv.conf /run/systemd/resolve/resolv.conf 2>/dev/null); do
iptables -w -A CI_SHOT -d "\$ns" -p udp --dport 53 -j ACCEPT
iptables -w -A CI_SHOT -d "\$ns" -p tcp --dport 53 -j ACCEPT
done
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
iptables -w -A CI_SHOT -d "\$net" -j REJECT
done
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
ip6tables -w -A CI_SHOT -d "\$net" -j REJECT
done
EOF
chmod +x /usr/local/sbin/ci-shot-firewall
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
[Unit]
Description=Keep the screenshot browser away from this host and the lab network
After=network-online.target incus.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/ci-shot-firewall
[Install]
WantedBy=multi-user.target
EOF
[[ -s /etc/timezone ]] || {
timedatectl set-timezone Europe/Vienna
echo Europe/Vienna >/etc/timezone
}
# The rewritten PR trees, served to the engine and to the containers. Waits
# for incusbr0, which only exists once Incus is up.
mkdir -p "$SERVE_ROOT"
cat >/etc/systemd/system/ci-serve.service <<EOF
[Unit]
Description=Serve pull request trees to script builds
After=incus.service network-online.target
Wants=incus.service
[Service]
ExecStartPre=/bin/sh -c 'for i in \$(seq 1 60); do ip -4 addr show dev incusbr0 | grep -q ${NET_ADDR} && exit 0; incus network list >/dev/null 2>&1; sleep 1; done; exit 1'
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind ${NET_ADDR} --directory ${SERVE_ROOT}
DynamicUser=yes
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
# Leftovers of a job that was killed before its cleanup trap ran.
cat >/usr/local/sbin/ci-sweep <<'EOF'
#!/usr/bin/env bash
# Delete CI containers (ci-9000 .. ci-9099) that no running job holds, and stale trees.
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
# restart, timeout) never removes it, so a lock older than any job can run
# (90 minutes) holds nothing.
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
for name in $(incus list -c n -f csv 2>/dev/null | grep -E '^ci-9[0-9]{3}$'); do
held "$name" && continue
incus delete "$name" --force &>/dev/null && echo "swept $name"
done
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
find /usr/local/community-scripts/logs /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
true
EOF
chmod +x /usr/local/sbin/ci-sweep
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
[Unit]
Description=Remove leftover CI containers
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ci-sweep
EOF
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
[Unit]
Description=Remove leftover CI containers every 3 hours
[Timer]
OnCalendar=0/3:30
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
systemctl restart ci-serve.service ci-shot-firewall.service
# --- Runner (one per repository) ----------------------------------------------
if [[ $# -eq 2 ]]; then
repo="$1"
token="$2"
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
dir="/opt/actions-runner/${repo#*/}"
mkdir -p "$dir"
cd "$dir"
if [[ ! -x ./config.sh ]]; then
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
./bin/installdependencies.sh >/dev/null
fi
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
# a fresh runner for every job and resets the VM afterwards, so only the
# runner software goes on it, and the 'clean' snapshot is taken after this.
if [[ "$token" == jit ]]; then
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
exit 0
fi
if [[ -f .runner ]]; then
./svc.sh stop >/dev/null 2>&1 || true
./svc.sh uninstall >/dev/null 2>&1 || true
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
fi
# Only `incus-test`: see pve-ci-host-setup.sh for why there are no default labels.
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
--url "https://github.com/${repo}" --token "$token" \
--name "$(hostname -s)-${repo#*/}" --labels incus-test --work _work
./svc.sh install root >/dev/null
./svc.sh start >/dev/null
echo "runner for ${repo} registered"
fi
echo "incus test host ready: ${SERVE_ROOT} on ${NET_ADDR}:${SERVE_PORT}, sweeper every 3h"
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env bash
# One-time setup of the Proxmox test host behind pr-script-test.yml.
# Safe to re-run. Run as root on the test host (a disposable PVE, never a
# production node: it builds pull request code as root).
#
# pve-ci-host-setup.sh host only
# pve-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
# pve-ci-host-setup.sh <owner/repo> jit host + runner software only, for
# ci-vm-controller.sh (single-use host)
#
# The token is a runner *registration* token (valid one hour):
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
set -euo pipefail
RUNNER_VERSION="${RUNNER_VERSION:-2.338.2}"
SERVE_ROOT=/srv/ci
SERVE_PORT=18080
UPLINK="${UPLINK:-vmbr0}"
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq jq curl python3 dnsmasq iptables chromium fonts-dejavu fonts-noto-color-emoji >/dev/null
# Screenshots of the app under test are taken as this user, never as root.
id ci-shot &>/dev/null || useradd --system --create-home --shell /usr/sbin/nologin ci-shot
# The page Chromium renders is served by code under test, and a redirect or an
# <img> could point it at this host (the PVE UI on :8006) or the lab network;
# the screenshot would then publish that on the PR. So ci-shot may reach the
# test containers, DNS and the public internet (apps load fonts and scripts
# from CDNs), and nothing private.
cat >/usr/local/sbin/ci-shot-firewall <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
uid=$(id -u ci-shot)
for fw in iptables ip6tables; do
$fw -w -N CI_SHOT 2>/dev/null || $fw -w -F CI_SHOT
$fw -w -C OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT 2>/dev/null ||
$fw -w -I OUTPUT -m owner --uid-owner "$uid" -j CI_SHOT
done
iptables -w -A CI_SHOT -d 10.99.0.1 -j REJECT
iptables -w -A CI_SHOT -d 10.99.0.0/24 -j ACCEPT
for ns in $(awk '/^nameserver/ && $2 !~ /:/ {print $2}' /etc/resolv.conf); do
iptables -w -A CI_SHOT -d "$ns" -p udp --dport 53 -j ACCEPT
iptables -w -A CI_SHOT -d "$ns" -p tcp --dport 53 -j ACCEPT
done
for net in 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/3; do
iptables -w -A CI_SHOT -d "$net" -j REJECT
done
for net in ::1/128 fc00::/7 fe80::/10 ff00::/8; do
ip6tables -w -A CI_SHOT -d "$net" -j REJECT
done
EOF
chmod +x /usr/local/sbin/ci-shot-firewall
cat >/etc/systemd/system/ci-shot-firewall.service <<'EOF'
[Unit]
Description=Keep the screenshot browser away from this host and the lab network
After=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/ci-shot-firewall
[Install]
WantedBy=multi-user.target
EOF
# Containers get their own NATed bridge instead of the lab network: no DHCP
# lease surprises (a lab lease once handed a container the host's own address),
# and an app listening on 8080 can never be confused with this host.
cat >/etc/network/interfaces.d/ci-nat <<EOF
auto vmbr1
iface vmbr1 inet static
address 10.99.0.1/24
bridge-ports none
bridge-stp off
bridge-fd 0
post-up sysctl -qw net.ipv4.ip_forward=1
post-up iptables -t nat -C POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE
post-down iptables -t nat -D POSTROUTING -s 10.99.0.0/24 -o ${UPLINK} -j MASQUERADE || true
EOF
ifreload -a
cat >/etc/dnsmasq.d/ci.conf <<'EOF'
interface=vmbr1
bind-interfaces
except-interface=lo
dhcp-range=10.99.0.100,10.99.0.250,255.255.255.0,2h
dhcp-option=option:router,10.99.0.1
dhcp-option=option:dns-server,10.99.0.1
no-resolv
EOF
# Upstream = whatever this host resolves with. Debian's dnsmasq would otherwise
# ask resolvconf, which has nothing to offer on a PVE host.
awk '/^nameserver/ {print "server=" $2}' /etc/resolv.conf >>/etc/dnsmasq.d/ci.conf
systemctl enable dnsmasq >/dev/null
systemctl restart dnsmasq
# A PVE ISO install writes /etc/timezone; a cloud-image install does not, and
# the engine's pct_timezone aborts without it.
[[ -s /etc/timezone ]] || {
timedatectl set-timezone Europe/Vienna
echo Europe/Vienna >/etc/timezone
}
# Containers and VM disks live on the root filesystem; there is no LVM in a
# cloud-image PVE. Snippets hold the Cloud-Init configs some vm/ scripts write.
pvesm set local --content iso,vztmpl,backup,rootdir,images,snippets
# Templates are fetched once here rather than on every first build.
pveam update >/dev/null
for t in $(pveam available --section system | awk '/_amd64\./ && /debian-13-standard|alpine-3\.2[0-9]-default|ubuntu-24\.04-standard/ {print $2}'); do
pveam list local | grep -q "$t" || pveam download local "$t" >/dev/null
done
# The rewritten PR trees, served to the engine and to the containers.
mkdir -p "$SERVE_ROOT"
cat >/etc/systemd/system/ci-serve.service <<EOF
[Unit]
Description=Serve pull request trees to script builds
After=network-online.target
[Service]
ExecStart=/usr/bin/python3 -m http.server ${SERVE_PORT} --bind 10.99.0.1 --directory ${SERVE_ROOT}
DynamicUser=yes
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# Leftovers of a job that was killed before its cleanup trap ran.
cat >/usr/local/sbin/ci-sweep <<'EOF'
#!/usr/bin/env bash
# Destroy CI containers (9000-9099) and VMs (9100-9199) that no running job
# holds, and stale trees.
# A job holds its guest with /run/ci-jobs/<id>. A job killed outright (runner
# restart, timeout) never removes it, so a lock older than any job can run
# (90 minutes) holds nothing.
held() { [[ -e "/run/ci-jobs/$1" && -z "$(find "/run/ci-jobs/$1" -mmin +180)" ]]; }
for id in $(pct list | awk 'NR>1 && $1>=9000 && $1<=9099 {print $1}'); do
held "$id" && continue
pct stop "$id" --skiplock 1 &>/dev/null || true
pct destroy "$id" --purge 1 --force 1 &>/dev/null && echo "swept CT $id"
done
for id in $(qm list | awk 'NR>1 && $1>=9100 && $1<=9199 {print $1}'); do
held "$id" && continue
qm stop "$id" --skiplock 1 --timeout 30 &>/dev/null || true
qm destroy "$id" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null && echo "swept VM $id"
done
# VM images and ISOs are cached between builds; drop the ones no build used
# for a week. Container templates (*.tar.*) stay.
find /var/lib/vz/template/cache /var/lib/vz/template/iso -maxdepth 1 -type f ! -name '*.tar.*' -atime +7 -delete 2>/dev/null
find /run/ci-jobs -maxdepth 1 -type f ! -name ".*" -mmin +180 -delete 2>/dev/null
find /srv/ci -mindepth 1 -maxdepth 1 -type d -mmin +180 -exec rm -rf {} +
# Engine logs that a killed job never got to remove.
find /tmp -maxdepth 1 -type f \( -name 'create-lxc-*.log' -o -name 'pct_create_*.log' \
-o -regex '/tmp/.*-9[0-9][0-9][0-9]-[0-9a-f]+\.log' \) -mmin +180 -delete
find /var/log/community-scripts -type f -mtime +2 -delete 2>/dev/null
find /opt/actions-runner/*/_diag -type f -mtime +14 -delete 2>/dev/null
true
EOF
chmod +x /usr/local/sbin/ci-sweep
cat >/etc/systemd/system/ci-sweep.service <<'EOF'
[Unit]
Description=Remove leftover CI containers
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ci-sweep
EOF
cat >/etc/systemd/system/ci-sweep.timer <<'EOF'
[Unit]
Description=Remove leftover CI containers every 3 hours
[Timer]
OnCalendar=0/3:30
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now ci-serve.service ci-sweep.timer ci-shot-firewall.service >/dev/null
systemctl restart ci-serve.service ci-shot-firewall.service
# --- Runner (one per repository) ----------------------------------------------
if [[ $# -eq 2 ]]; then
repo="$1"
token="$2"
[[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || { echo "bad repo" >&2; exit 2; }
dir="/opt/actions-runner/${repo#*/}"
mkdir -p "$dir"
cd "$dir"
if [[ ! -x ./config.sh ]]; then
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
./bin/installdependencies.sh >/dev/null
fi
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
# a fresh runner for every job and resets the VM afterwards, so only the
# runner software goes on it, and the 'clean' snapshot is taken after this.
if [[ "$token" == jit ]]; then
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
exit 0
fi
# A registered runner is reconfigured from scratch: stop and remove the old
# registration (the token works for removal too).
if [[ -f .runner ]]; then
./svc.sh stop >/dev/null 2>&1 || true
./svc.sh uninstall >/dev/null 2>&1 || true
RUNNER_ALLOW_RUNASROOT=1 ./config.sh remove --token "$token" >/dev/null 2>&1 || true
fi
# Root, because pct needs it; the host is disposable by design.
# --no-default-labels: without it the runner also answers to `self-hosted`,
# and any other workflow with `runs-on: self-hosted` (and its secrets) could
# land on this host.
# Named after this host: with --replace, a fixed name would make a second
# test host take over the first one's registration instead of adding to it.
RUNNER_ALLOW_RUNASROOT=1 ./config.sh --unattended --replace --no-default-labels \
--url "https://github.com/${repo}" --token "$token" \
--name "$(hostname -s)-${repo#*/}" --labels proxmox-test --work _work
./svc.sh install root >/dev/null
./svc.sh start >/dev/null
echo "runner for ${repo} registered"
fi
echo "test host ready: ${SERVE_ROOT} on :${SERVE_PORT}, sweeper every 3h"
+645
View File
@@ -0,0 +1,645 @@
#!/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. Apps deployed from a branch (fetch_and_deploy_gh_branch, checked
# with check_for_gh_branch) store a commit hash instead of a version; the
# engine compares both as plain strings, so 0.0.0-ci works for them too. `update` is the /usr/bin/update the install generated: it pulls
# ct/<app>.sh from the URL this container was built from, i.e. the PR's tree.
UPDATE_FAKE="0.0.0-ci"
update_fail() {
update_status="fail"
update_step="$1"
fail "update: $1"
}
version_files=$(guest_exec sh -c '
for f in /root/.*; do
[ -f "$f" ] || continue
case "$f" in */.install-* | *.pending | *.bak | */.bash* | */.profile | */.hushlogin | */.viminfo | */.lesshst | */.wget-hsts | */.selected_editor) continue ;; esac
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^(v?[0-9]+(\.[0-9A-Za-z_+-]+)+|[0-9a-f]{7,40})$" "$f" && echo "$f"
done' 2>/dev/null | grep -E '^/root/\.[A-Za-z0-9._-]+$')
# Only set back what update_script checks: the names it passes to
# check_for_*release. The install can also deploy helpers the update never
# touches (immich: ~/.mise); those would stay 0.0.0-ci and fail the run. A
# name that is a variable can't be read here, so then all of them are set back.
tracked=$(grep -oE 'check_for_[a-z]*_?release[[:space:]]+("[^"]*"|[^[:space:]]+)' "$tree/$SCRIPT" |
sed -E 's/^check_for_[a-z]*_?release[[:space:]]+//; s/^"(.*)"$/\1/' | tr -d ' ' | tr '[:upper:]' '[:lower:]' | sort -u)
if [[ -n "$tracked" && "$tracked" != *'$'* ]]; then
version_files=$(grep -xF -f <(sed 's|^|/root/.|' <<<"$tracked") <<<"$version_files" || true)
fi
for f in $version_files; do
guest_exec sh -c "echo '$UPDATE_FAKE' > '$f'; rm -f '$f.pending'" ||
update_fail "could not reset $f"
done
[[ -n "$version_files" ]] && echo "Update test: set back $(tr '\n' ' ' <<<"$version_files")to ${UPDATE_FAKE}"
: >"$UPDATE_RAW"
update_token=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n')
echo "──────── update test (live) ────────"
echo "::stop-commands::${update_token}"
# timeout runs a program, not a shell function, so spell out the exec here.
if [[ "$PLATFORM" == "incus" ]]; then guest_cmd=(incus exec "$ctid" --); else guest_cmd=(pct exec "$ctid" --); fi
timeout --kill-after=1m "$UPDATE_TIMEOUT" "${guest_cmd[@]}" env PHS_SILENT=1 mode=default MODE=default \
TERM=dumb DIAGNOSTICS=no var_ignore_breaking_changes=yes ${GH_API_TOKEN:+GITHUB_TOKEN="$GH_API_TOKEN"} update </dev/null \
> >(tee "$UPDATE_RAW" | live_filter) 2>&1 &
update_pid=$!
wait "$update_pid"
update_rc=$?
update_pid=""
sleep 1
echo "::${update_token}::"
update_failure_line() {
strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]|error:' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180
}
if ((update_rc == 124 || update_rc == 137)); then
update_fail "timed out after ${UPDATE_TIMEOUT}"
elif ((update_rc != 0)); then
last=$(update_failure_line)
update_fail "exited ${update_rc}${last:+: $last}"
fi
# The engine can report an error and still exit 0 (bark-server: "github: rate
# limit reached", then "Cleaned"). A rate limit is ours, not the PR's: the
# update could not be tested. Any other error line is a failed update.
update_error=$(strip_ansi <"$UPDATE_RAW" | grep -E '✖|\[ERROR\]' | tail -n 1 | sed -E 's/^\s*(✖️?|\s)*//' | cut -c1-180)
if [[ -n "$update_error" ]]; then
if [[ "${update_error,,}" == *"rate limit"* ]]; then
update_status="error"
update_step="could not be tested: ${update_error}"
status="pass"
finish
exit 0
fi
update_fail "$update_error"
fi
if strip_ansi <"$UPDATE_RAW" | grep -qi 'updated successfully'; then
update_status="updated"
for f in $version_files; do
[[ "$(guest_exec cat "$f" 2>/dev/null)" != "$UPDATE_FAKE" ]] ||
update_fail "reported success, but $f is still ${UPDATE_FAKE}"
done
else
# Ran cleanly without updating anything: no version file to fake (apt/npm
# based), or an update_script that decides differently. Not a failure.
update_status="no-update"
update_step=$(strip_ansi <"$UPDATE_RAW" | grep -iE 'no update|up to date|up-to-date|already' | tail -n 1 | sed -E 's/^\s*(✔️?|ℹ️?|\s)*//' | cut -c1-160)
fi
check_health screenshot-update.png || update_fail "after the update: $health_problem"
status="pass"
finish
+140
View File
@@ -0,0 +1,140 @@
# shellcheck shell=bash
# The vm/ half of pve-run-script.sh, sourced from it: same inputs, same
# result.json, same live output. Only building and checking differ.
#
# A VM script runs entirely on this host (there is no install script inside
# the guest to follow), unattended through the core engine's VM_UNATTENDED=1:
# the script's defaults, with ID, name, bridge and storage set from here. The
# guest then boots on vmbr1 like the containers do, so its address is whatever
# this host's dnsmasq leased to its MAC; no guest agent needed.
#
# What passes: the script exits 0 and says "Completed successfully", the VM is
# running and keeps running through the boot wait, and where the script prints
# a URL built from the VM's address (http://${VM_IP}:9000) that URL answers 200.
# An installer ISO or an image without DHCP gets no address; that is reported,
# not failed. There is no update test: a VM has no `update`.
VM_BOOT_WAIT="${VM_BOOT_WAIT:-300}"
VM_URL_WAIT="${VM_URL_WAIT:-600}"
LEASES="${LEASES:-/var/lib/misc/dnsmasq.leases}"
# One VM build per host at a time: a VM takes 2-8 GB of RAM and a disk image,
# where a container takes a fraction. Held until the job exits; the build is
# started with this descriptor closed, so a QEMU process can never inherit it.
vm_wait_turn() {
exec 7>/run/ci-jobs/.vm-turn
flock -n 7 && return 0
echo "Another VM build is running on this host; waiting for it (one VM at a time)"
flock -w 3600 7 || setup_error "waited an hour for this host's VM slot"
}
vm_cleanup() {
if qm status "$ctid" &>/dev/null; then
qm stop "$ctid" --skiplock 1 --timeout 30 &>/dev/null || true
qm destroy "$ctid" --purge 1 --destroy-unreferenced-disks 1 --skiplock 1 &>/dev/null || true
fi
rm -f "/run/ci-jobs/$ctid" "/run/ci-jobs/console-${ctid}.png"
# Cloud-Init network snippets the script wrote for this VM.
[[ -f "$RAW_LOG.start" ]] &&
find /var/lib/vz/snippets -maxdepth 1 -type f -name "*${ctid}*" -newer "$RAW_LOG.start" -delete 2>/dev/null
true
}
# The VM's screen, straight from QEMU. Pixels only, but they come from the
# guest, so the same checks as for a browser screenshot apply.
vm_console_shot() {
local f="/run/ci-jobs/console-${ctid}.png"
rm -f "$f"
printf 'screendump %s -f png\n' "$f" | timeout 30 qm monitor "$ctid" &>/dev/null
if [[ -s "$f" && $(stat -c %s "$f") -lt 3000000 ]] &&
[[ "$(head -c 8 "$f" | od -An -tx1 | tr -d ' \n')" == "89504e470d0a1a0a" ]]; then
cp "$f" "$OUT_DIR/screenshot.png"
fi
rm -f "$f"
}
vm_running() {
qm status "$ctid" 2>/dev/null | grep -q 'status: running'
}
vm_build_and_check() {
local hn mac pattern waited last
hn="ci-$(tr -c 'a-z0-9-\n' '-' <<<"${app,,}" | cut -c1-40)-${ctid}"
echo "Building ${SCRIPT} as VM ${ctid} from ${PR_REPO}@${PR_SHA:0:7}"
local vm_env=(
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/root TERM=dumb LANG=C.UTF-8 DIAGNOSTICS=no
VM_UNATTENDED=1 VM_VMID="$ctid" VM_HOSTNAME="$hn" VM_BRIDGE="$BRIDGE"
VM_STORAGE=local VM_ISO_STORAGE=local VM_START=yes
COMMUNITY_SCRIPTS_URL="$base"
)
# Release lookups (vm_latest_from_github and the like).
[[ -n "${GH_API_TOKEN:-}" ]] && vm_env+=(GITHUB_TOKEN="$GH_API_TOKEN")
touch "$RAW_LOG.start"
env -i "${vm_env[@]}" timeout --kill-after=1m "$BUILD_TIMEOUT" \
bash -c 'bash -c "$(curl -fsSL "$1")"' _ "$base/$SCRIPT" </dev/null >"$RAW_LOG" 2>&1 7>&- &
build_pid=$!
start_streams
wait "$build_pid"
exit_code=$?
build_pid=""
stop_streams
((exit_code == 124 || exit_code == 137)) && fail "build timed out after ${BUILD_TIMEOUT}"
if ((exit_code != 0)); then
last=$(failure_line)
step="script exited ${exit_code}${last:+: $last}"
status="fail"
classify_cpu_failure
fail "$step" "$status"
fi
# vm_finish ends on a msg_ok of the script's choosing ("VM created. ..."),
# older scripts on "Completed successfully". exit_script and friends leave
# with 0 too, but on a cross.
last=$(strip_ansi <"$RAW_LOG" | tail -n 1 | cut -c1-200)
if ! grep -qi 'completed successfully' "$RAW_LOG" && ! [[ "$last" =~ ^[[:space:]]*✔ ]]; then
fail "script exited 0 but never reported success${last:+ (last line: ${last})}"
fi
vm_running || fail "VM ${ctid} is not running after the script finished"
# The address dnsmasq handed to this VM's MAC. The VM must stay up while it
# boots: one that powers itself off or crashes is a failure.
mac=$(qm config "$ctid" 2>/dev/null | sed -nE 's/^net0:[^=]*=([0-9A-Fa-f:]{17}).*/\1/p' | tr 'A-F' 'a-f')
echo "Waiting up to ${VM_BOOT_WAIT}s for VM ${ctid} (${mac:-no NIC}) to boot and get an address"
for ((waited = 0; waited < VM_BOOT_WAIT; waited += 5)); do
vm_running || fail "VM ${ctid} stopped by itself while booting"
[[ -n "$mac" ]] && ct_ip=$(awk -v m="$mac" '$2 == m {print $3}' "$LEASES" 2>/dev/null | tail -n 1)
[[ -n "$ct_ip" ]] && break
sleep 5
done
[[ -n "$ct_ip" ]] && echo "VM ${ctid} got ${ct_ip}" || echo "VM ${ctid} got no DHCP lease within ${VM_BOOT_WAIT}s"
# Only a URL the script builds from the address it found; <VM-IP> in a
# summary is a hint for later (after an installer ran), not a promise.
# ${VM_IP}, $VM_IP and ${VM_IP:-<VM-IP>} all count.
local ip_re='\$\{?VM_IP(:-[^}]*)?\}?'
pattern=$(grep -oE "https?://${ip_re}(:[0-9]+)?(/[A-Za-z0-9._~/?=&%-]*)?" "$tree/$SCRIPT" | head -n1)
if [[ -n "$pattern" ]]; then
[[ -n "$ct_ip" ]] || fail "no IP address within ${VM_BOOT_WAIT}s, so ${pattern} could not be checked"
url=$(sed -E "s#${ip_re}#${ct_ip}#" <<<"$pattern")
for ((waited = 0; waited < VM_URL_WAIT; waited += 10)); do
vm_running || fail "VM ${ctid} stopped by itself while its web UI was awaited"
http_code=$(curl -sk -L --max-redirs 5 -o /dev/null -w '%{http_code}' --max-time 10 "$url" || true)
[[ "$http_code" =~ ^2[0-9][0-9]$ ]] && break
sleep 10
done
[[ "$http_code" =~ ^[1-5][0-9][0-9]$ ]] && take_screenshot screenshot.png
else
# Let the console settle on a login prompt rather than a boot message.
sleep 20
fi
[[ -s "$OUT_DIR/screenshot.png" ]] || vm_console_shot
if [[ -n "$url" && ! "$http_code" =~ ^2[0-9][0-9]$ ]]; then
fail "${url} answered ${http_code/#000/nothing} instead of 200 within ${VM_URL_WAIT}s"
fi
vm_running || fail "VM ${ctid} stopped by itself"
status="pass"
finish
}
+95 -73
View File
@@ -22,90 +22,112 @@ jobs:
const now = new Date();
const owner = context.repo.owner;
const repo = context.repo.repo;
// --- When stale label is added, comment immediately ---
const TEMPLATE_LABEL = "missing pr template";
const MAINTAINERS = ["OWNER", "MEMBER", "COLLABORATOR"];
const days = d => (now - d) / (1000 * 60 * 60 * 24);
const staleNotice = author =>
`@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`;
// Only fires for a maintainer's label: labels set with GITHUB_TOKEN raise no event.
if (context.eventName === "pull_request_target" && context.payload.action === "labeled") {
const label = context.payload.label?.name;
if (label === "stale") {
const author = context.payload.pull_request.user.login;
const pr = context.payload.pull_request;
const labels = (pr.labels || []).map(l => l.name);
if (context.payload.label?.name === "stale" && !labels.includes(TEMPLATE_LABEL)) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: context.payload.pull_request.number,
body: `@${author} This PR has been marked as stale. It will be closed if no new commits are added in 7 days.`
owner, repo, issue_number: pr.number, body: staleNotice(pr.user.login)
});
}
return;
}
// --- Scheduled run: check all stale PRs ---
const { data: prs } = await github.rest.pulls.list({
owner,
repo,
state: "open",
per_page: 100
});
const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: "open", per_page: 100 });
for (const pr of prs) {
const hasStale = pr.labels.some(l => l.name === "stale");
if (!hasStale) continue;
// Get timeline events to find when stale label was added
const { data: events } = await github.rest.issues.listEvents({
owner,
repo,
issue_number: pr.number,
per_page: 100
});
// Find the most recent time the stale label was added
const staleLabelEvents = events
.filter(e => e.event === "labeled" && e.label?.name === "stale")
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
if (staleLabelEvents.length === 0) continue;
const staleLabelDate = new Date(staleLabelEvents[0].created_at);
const daysSinceStale = (now - staleLabelDate) / (1000 * 60 * 60 * 24);
// Check for new commits since stale label was added
const { data: commits } = await github.rest.pulls.listCommits({
owner,
repo,
pull_number: pr.number
});
const lastCommitDate = new Date(commits[commits.length - 1].commit.author.date);
const labels = pr.labels.map(l => l.name);
const author = pr.user.login;
// If there are new commits after the stale label, remove it
if (lastCommitDate > staleLabelDate) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr.number,
name: "stale"
const hasStale = labels.includes("stale");
if (labels.includes("keep-open")) {
if (hasStale) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
}
continue;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100
});
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner, repo, pull_number: pr.number, per_page: 100
});
const lastCommitDate = commits.length > 0
? new Date(commits[commits.length - 1].commit.author.date)
: new Date(pr.created_at);
const humanComments = comments.filter(c => c.user?.type !== "Bot");
// -------------------------------------------------------
// Auto-label PRs with no activity in the last 14 days,
// but ONLY if someone other than the author has engaged.
// -------------------------------------------------------
if (!hasStale) {
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner, repo, pull_number: pr.number, per_page: 100
});
const engaged =
reviews.some(r => r.user?.type !== "Bot" && r.user?.login !== author) ||
humanComments.some(c => c.user?.login !== author);
if (!engaged) continue;
const lastCommentDate = humanComments.length > 0
? new Date(humanComments[humanComments.length - 1].created_at)
: null;
const lastActivityDate = lastCommentDate && lastCommentDate > lastCommitDate
? lastCommentDate
: lastCommitDate;
if (days(lastActivityDate) > 14) {
await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: ["stale"] });
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: staleNotice(author) });
}
continue;
}
// -------------------------------------------------------
// Manage already-stale PRs
// -------------------------------------------------------
const events = await github.paginate(github.rest.issues.listEvents, {
owner, repo, issue_number: pr.number, per_page: 100
});
const staleDates = events
.filter(e => e.event === "labeled" && e.label?.name === "stale")
.map(e => new Date(e.created_at));
if (staleDates.length === 0) continue;
const staleLabelDate = new Date(Math.max(...staleDates));
const isTemplateIssue = labels.includes(TEMPLATE_LABEL);
// A push ends an inactivity mark; a template mark only ends via close-invalid-pr-template.yml's re-check.
if (!isTemplateIssue && lastCommitDate > staleLabelDate) {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pr.number, name: "stale" });
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
owner, repo, issue_number: pr.number,
body: `@${author} Recent activity detected. Removing stale label.`
});
continue;
}
// If 7 days have passed since stale label, close the PR
else if (daysSinceStale > 7) {
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: "closed"
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
});
}
// A maintainer comment after the mark restarts the 7 days.
const maintainerDates = humanComments
.filter(c => MAINTAINERS.includes(c.author_association) && c.user?.login !== author)
.map(c => new Date(c.created_at))
.filter(d => d > staleLabelDate);
const clockStart = maintainerDates.length > 0 ? new Date(Math.max(...maintainerDates)) : staleLabelDate;
if (days(clockStart) <= 7) continue;
await github.rest.issues.createComment({
owner, repo, issue_number: pr.number,
body: isTemplateIssue
? `@${author} Closing: the PR template requirements listed above were still missing 7 days later. Once they are in place, please open a new pull request.`
: `@${author} Closing stale PR due to inactivity (no commits for 7 days after stale label).`
});
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: "closed" });
}
+268 -186
View File
@@ -125,6 +125,9 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
@@ -137,6 +140,13 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
<summary><h3>2026</h3></summary>
<details>
<summary><h4>November (7 entries)</h4></summary>
[View November 2026 Changelog](.github/changelogs/2026/11.md)
</details>
<details>
<summary><h4>October (31 entries)</h4></summary>
@@ -571,14 +581,248 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-11-12
### 🆕 New Scripts
- Pixelfed ([#18523](https://github.com/community-scripts/ProxmoxVE/pull/18523))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Whisparr-eros: set the first login and save it [@MickLesk](https://github.com/MickLesk) ([#18617](https://github.com/community-scripts/ProxmoxVE/pull/18617))
## 2026-11-11
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- TrueNas: Fixing isolookup and dialog [@juronja](https://github.com/juronja) ([#18578](https://github.com/community-scripts/ProxmoxVE/pull/18578))
- #### ✨ New Features
- Immich: bump to 3.4.1 [@MickLesk](https://github.com/MickLesk) ([#18619](https://github.com/community-scripts/ProxmoxVE/pull/18619))
- Immich: bump to 3.4.0 [@MickLesk](https://github.com/MickLesk) ([#18600](https://github.com/community-scripts/ProxmoxVE/pull/18600))
- #### 💥 Breaking Changes
- Pocketbase: install and update from the new host (removed in github) [@MickLesk](https://github.com/MickLesk) ([#18609](https://github.com/community-scripts/ProxmoxVE/pull/18609))
- #### 🔧 Refactor
- Paperless-AI: install and update to 4.0 [@MickLesk](https://github.com/MickLesk) ([#18620](https://github.com/community-scripts/ProxmoxVE/pull/18620))
### 💾 Core
- core: drop the SUDO_ variables an update through sudo carries [@MickLesk](https://github.com/MickLesk) ([core#229](https://github.com/community-scripts/core/pull/229))
- feat: let setup_deb822_repo use a keyring already on the system [@MickLesk](https://github.com/MickLesk) ([core#227](https://github.com/community-scripts/core/pull/227))
### 📚 Documentation
- Fill platforms when a script moves from DevScripts [@MickLesk](https://github.com/MickLesk) ([#18611](https://github.com/community-scripts/ProxmoxVE/pull/18611))
## 2026-11-10
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- writefreely: Force symbolic link update [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18588](https://github.com/community-scripts/ProxmoxVE/pull/18588))
- openproject: start memcached when IPv6 is disabled [@MickLesk](https://github.com/MickLesk) ([#18584](https://github.com/community-scripts/ProxmoxVE/pull/18584))
- homeassistant.sh: Update pip install command for runlike [@Zajsoft](https://github.com/Zajsoft) ([#18583](https://github.com/community-scripts/ProxmoxVE/pull/18583))
- Tunarr: fix install and update for the new release layout and latest ErsatzTV-ffmpeg [@chrisbenincasa](https://github.com/chrisbenincasa) ([#18577](https://github.com/community-scripts/ProxmoxVE/pull/18577))
### 💾 Core
- fix: keep a script's config file when a package installs it [@MickLesk](https://github.com/MickLesk) ([core#226](https://github.com/community-scripts/core/pull/226))
- fix: flush the image before importing a VM disk [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([core#223](https://github.com/community-scripts/core/pull/223))
### 📂 Github
- pr-script-test: pre-answer prompt variables built in a loop [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18596](https://github.com/community-scripts/ProxmoxVE/pull/18596))
- Build PR scripts on a Proxmox test host via /test [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18594](https://github.com/community-scripts/ProxmoxVE/pull/18594))
## 2026-11-09
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- zipline: Update RAM to 3GB [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18569](https://github.com/community-scripts/ProxmoxVE/pull/18569))
- OpenObserve: record installed version, drop killall ExecStop [@BadFlo](https://github.com/BadFlo) ([#18566](https://github.com/community-scripts/ProxmoxVE/pull/18566))
- OpenObserve: extract release tarball with --no-same-owner [@BadFlo](https://github.com/BadFlo) ([#18564](https://github.com/community-scripts/ProxmoxVE/pull/18564))
- Immich: download before stopping, and clear maintenance mode after an interrupted update [@angusmaul](https://github.com/angusmaul) ([#18553](https://github.com/community-scripts/ProxmoxVE/pull/18553))
- cinephage: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18374](https://github.com/community-scripts/ProxmoxVE/pull/18374))
- kan: bump Node.js from 20 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18549](https://github.com/community-scripts/ProxmoxVE/pull/18549))
- myip: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#18550](https://github.com/community-scripts/ProxmoxVE/pull/18550))
- #### 🔧 Refactor
- security: remove hardcoded credentials in several scripts [@MickLesk](https://github.com/MickLesk) ([#18572](https://github.com/community-scripts/ProxmoxVE/pull/18572))
- yuvomi: install from the attested release asset [@ulsklyc](https://github.com/ulsklyc) ([#18554](https://github.com/community-scripts/ProxmoxVE/pull/18554))
### 🗑️ Deleted Scripts
- coolify: delete old ct / its an addon [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#18562](https://github.com/community-scripts/ProxmoxVE/pull/18562))
### 💾 Core
- core: detect GPUs by their PCI class, not by text anywhere in the line [@MickLesk](https://github.com/MickLesk) ([core#221](https://github.com/community-scripts/core/pull/221))
- core: load an app's own settings from its .vars file [@MickLesk](https://github.com/MickLesk) ([core#216](https://github.com/community-scripts/core/pull/216))
### 🧰 Tools
- #### 🐞 Bug Fixes
- IP-Tag: filter guest-agent IPs by CIDR before limiting to 3 [@mazelado](https://github.com/mazelado) ([#18537](https://github.com/community-scripts/ProxmoxVE/pull/18537))
## 2026-11-08
### 🆕 New Scripts
- Chatto ([#18521](https://github.com/community-scripts/ProxmoxVE/pull/18521))
- Firefox-Syncserver ([#18520](https://github.com/community-scripts/ProxmoxVE/pull/18520))
### 🚀 Updated Scripts
- #### 🔧 Refactor
- Immich: do not clone libjxl's testdata [@MickLesk](https://github.com/MickLesk) ([#18539](https://github.com/community-scripts/ProxmoxVE/pull/18539))
- Databasus: build 3.65 and ship the verification agent [@MickLesk](https://github.com/MickLesk) ([#18500](https://github.com/community-scripts/ProxmoxVE/pull/18500))
### 💾 Core
- fix: a closed console no longer ends a running setup | ERR trap fixes | check VM ID before creating [@MickLesk](https://github.com/MickLesk) ([core#219](https://github.com/community-scripts/core/pull/219))
### 🧰 Tools
- #### 🐞 Bug Fixes
- update-apps: keep the backup alive on a closed console, stop before restoring [@MickLesk](https://github.com/MickLesk) ([#18540](https://github.com/community-scripts/ProxmoxVE/pull/18540))
- clean-lxcs: run with sh and keep going when one container fails [@MickLesk](https://github.com/MickLesk) ([#18538](https://github.com/community-scripts/ProxmoxVE/pull/18538))
- update-lxcs: keep going when one container fails [@MickLesk](https://github.com/MickLesk) ([#18541](https://github.com/community-scripts/ProxmoxVE/pull/18541))
## 2026-11-07
### 🆕 New Scripts
- EspoCRM ([#18519](https://github.com/community-scripts/ProxmoxVE/pull/18519))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- RomM: emit player isolation headers from the serving location [@adotagarwal](https://github.com/adotagarwal) ([#18511](https://github.com/community-scripts/ProxmoxVE/pull/18511))
- #### 💥 Breaking Changes
- fix: gotenberg: update to v9 [@CrazyWolf13](https://github.com/CrazyWolf13) ([#18491](https://github.com/community-scripts/ProxmoxVE/pull/18491))
### 💾 Core
- nvidia: close the hardware acceleration step when no GPU is usable | pin the whole NVIDIA userspace to the host's driver version [@MickLesk](https://github.com/MickLesk) ([core#215](https://github.com/community-scripts/core/pull/215))
- fix: stage release downloads on disk, not in /tmp [@rcourtman](https://github.com/rcourtman) ([core#218](https://github.com/community-scripts/core/pull/218))
- Minor fixes: restart php-fpm during sigpipe, Install jq for forge commands [@MickLesk](https://github.com/MickLesk) ([core#213](https://github.com/community-scripts/core/pull/213))
- incus: name the project restriction that refuses an Incus launch [@MickLesk](https://github.com/MickLesk) ([core#214](https://github.com/community-scripts/core/pull/214))
## 2026-11-06
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- UmbrelOS-VM: report runs as ProxmoxVE, not DevScripts [@MickLesk](https://github.com/MickLesk) ([#18497](https://github.com/community-scripts/ProxmoxVE/pull/18497))
- Caddy: skip the xCaddy test build below 2 GB RAM [@MickLesk](https://github.com/MickLesk) ([#18472](https://github.com/community-scripts/ProxmoxVE/pull/18472))
- Thingsboard: keep the package defaults in thingsboard.conf [@MickLesk](https://github.com/MickLesk) ([#18499](https://github.com/community-scripts/ProxmoxVE/pull/18499))
- Nexterm: keep the extracted app out of the /tmp cleanup [@MickLesk](https://github.com/MickLesk) ([#18501](https://github.com/community-scripts/ProxmoxVE/pull/18501))
- PLANKA: run the service with node instead of nodemon [@MickLesk](https://github.com/MickLesk) ([#18503](https://github.com/community-scripts/ProxmoxVE/pull/18503))
- MeTube/Yubal: install Deno without its shell setup [@MickLesk](https://github.com/MickLesk) ([#18406](https://github.com/community-scripts/ProxmoxVE/pull/18406))
- OPNsense VM: size the temp space check to the unpacked image | say why the EFI disk could not be allocated [@MickLesk](https://github.com/MickLesk) ([#18404](https://github.com/community-scripts/ProxmoxVE/pull/18404))
- #### 🔧 Refactor
- Technitium DNS: read the version from its GitHub releases [@MickLesk](https://github.com/MickLesk) ([#18425](https://github.com/community-scripts/ProxmoxVE/pull/18425))
### 💾 Core
- fix: spot same-filesystem bind mounts on Alpine [@rcourtman](https://github.com/rcourtman) ([core#212](https://github.com/community-scripts/core/pull/212))
- fix: report a container that went down during setup, keep finished installs [@MickLesk](https://github.com/MickLesk) ([core#211](https://github.com/community-scripts/core/pull/211))
### 📚 Documentation
- ci: reject DevScripts URLs in scripts [@MickLesk](https://github.com/MickLesk) ([#18498](https://github.com/community-scripts/ProxmoxVE/pull/18498))
## 2026-11-05
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- OpenWRT-VM: leave the NIC untagged on a bridge without a port [@MickLesk](https://github.com/MickLesk) ([#18473](https://github.com/community-scripts/ProxmoxVE/pull/18473))
- Podman: skip Portainer where Podman has no Quadlet [@MickLesk](https://github.com/MickLesk) ([#18469](https://github.com/community-scripts/ProxmoxVE/pull/18469))
- Planka: change npm start argument to --production [@CerberusStyle](https://github.com/CerberusStyle) ([#18478](https://github.com/community-scripts/ProxmoxVE/pull/18478))
- OPNsense VM: wait for the FreeBSD login prompt instead of 180 seconds [@MickLesk](https://github.com/MickLesk) ([#18467](https://github.com/community-scripts/ProxmoxVE/pull/18467))
- #### 🔧 Refactor
- truenas-VM: let a failed ISO lookup reach its own error [@MickLesk](https://github.com/MickLesk) ([#18476](https://github.com/community-scripts/ProxmoxVE/pull/18476))
- proxmox-mail-gateway: take the repo suite from the container [@MickLesk](https://github.com/MickLesk) ([#18475](https://github.com/community-scripts/ProxmoxVE/pull/18475))
- Immich: Bump to 3.3.6 and minor improvement [@MickLesk](https://github.com/MickLesk) ([#18474](https://github.com/community-scripts/ProxmoxVE/pull/18474))
- Tududi: run 2.0 on PostgreSQL, keep SQLite installs on 1.15.0 [@MickLesk](https://github.com/MickLesk) ([#18465](https://github.com/community-scripts/ProxmoxVE/pull/18465))
### 💾 Core
- fix: keep the existing install until the new archive has extracted [@rcourtman](https://github.com/rcourtman) ([core#201](https://github.com/community-scripts/core/pull/201))
- fix: keep the install when a clone or an Alpine extract fails [@rcourtman](https://github.com/rcourtman) ([core#210](https://github.com/community-scripts/core/pull/210))
### 🧰 Tools
- #### 🐞 Bug Fixes
- Arcane: write both env names, stop shipping the unused .env [@MickLesk](https://github.com/MickLesk) ([#18466](https://github.com/community-scripts/ProxmoxVE/pull/18466))
## 2026-11-04
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Teleport: Poll readyz instead of fixed 10 second sleep. [@cthach](https://github.com/cthach) ([#18461](https://github.com/community-scripts/ProxmoxVE/pull/18461))
- n8n: install past its unsatisfiable canvas dependencies [@MickLesk](https://github.com/MickLesk) ([#18439](https://github.com/community-scripts/ProxmoxVE/pull/18439))
- Teable: add Redis cache [@MickLesk](https://github.com/MickLesk) ([#18450](https://github.com/community-scripts/ProxmoxVE/pull/18450))
### 💾 Core
- feat: say why download_gpg_key failed [@MickLesk](https://github.com/MickLesk) ([core#208](https://github.com/community-scripts/core/pull/208))
- feat: read the MOTD's OS version at login [@MickLesk](https://github.com/MickLesk) ([core#207](https://github.com/community-scripts/core/pull/207))
- feat: skip screen clear when there is no terminal | pin only the scripts own app with var_appversion [@MickLesk](https://github.com/MickLesk) ([core#203](https://github.com/community-scripts/core/pull/203))
## 2026-11-03
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Fedora VM: let the engine install libguestfs-tools [@MickLesk](https://github.com/MickLesk) ([#18422](https://github.com/community-scripts/ProxmoxVE/pull/18422))
- Cinephage: run the camoufox binary the package now ships [@MickLesk](https://github.com/MickLesk) ([#18426](https://github.com/community-scripts/ProxmoxVE/pull/18426))
- AlmaLinux VM: report failures through the engine's error handler [@MickLesk](https://github.com/MickLesk) ([#18418](https://github.com/community-scripts/ProxmoxVE/pull/18418))
- WireGuard: give the Debian container 1 GB for WGDashboard [@MickLesk](https://github.com/MickLesk) ([#18419](https://github.com/community-scripts/ProxmoxVE/pull/18419))
- zabbix: use os specific apt packages [@MickLesk](https://github.com/MickLesk) ([#18417](https://github.com/community-scripts/ProxmoxVE/pull/18417))
- Discourse: build libvips 8.18 for its image worker [@MickLesk](https://github.com/MickLesk) ([#18430](https://github.com/community-scripts/ProxmoxVE/pull/18430))
- Wazuh: show the installer's own error when it fails [@MickLesk](https://github.com/MickLesk) ([#18428](https://github.com/community-scripts/ProxmoxVE/pull/18428))
- Tracearr: move to Node.js 26 [@connorgallopo](https://github.com/connorgallopo) ([#18393](https://github.com/community-scripts/ProxmoxVE/pull/18393))
- #### ✨ New Features
- Wiki.js: install 3.x [@MickLesk](https://github.com/MickLesk) ([#18382](https://github.com/community-scripts/ProxmoxVE/pull/18382))
- #### 🔧 Refactor
- OpenWrt VM: read the stable release from downloads.openwrt.org [@MickLesk](https://github.com/MickLesk) ([#18411](https://github.com/community-scripts/ProxmoxVE/pull/18411))
- Immich: fetch VectorChord from its new repo, skip Java, get GeoNames first [@MickLesk](https://github.com/MickLesk) ([#18397](https://github.com/community-scripts/ProxmoxVE/pull/18397))
### 💾 Core
- fix: put the reason for a VM failure into its trace [@MickLesk](https://github.com/MickLesk) ([core#200](https://github.com/community-scripts/core/pull/200))
@@ -595,6 +839,29 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- fix: restore npm when an interrupted update left it behind [@MickLesk](https://github.com/MickLesk) ([core#189](https://github.com/community-scripts/core/pull/189))
- feat: add var_cmode to set the container console mode [@MickLesk](https://github.com/MickLesk) ([core#180](https://github.com/community-scripts/core/pull/180))
### 🧰 Tools
- #### 🐞 Bug Fixes
- all-templates: answering No or Cancel is not a failure [@MickLesk](https://github.com/MickLesk) ([#18423](https://github.com/community-scripts/ProxmoxVE/pull/18423))
- homebrew: skip service accounts when picking the user, survive a failed apt update [@MickLesk](https://github.com/MickLesk) ([#18424](https://github.com/community-scripts/ProxmoxVE/pull/18424))
- add-tailscale-lxc: start a stopped container and check its OS first [@MickLesk](https://github.com/MickLesk) ([#18421](https://github.com/community-scripts/ProxmoxVE/pull/18421))
- netdata: survive a failed apt update, leave quietly on Cancel [@MickLesk](https://github.com/MickLesk) ([#18427](https://github.com/community-scripts/ProxmoxVE/pull/18427))
- Privilege-Converter: offer only container-capable storages as the converter's restore target [@MickLesk](https://github.com/MickLesk) ([#18415](https://github.com/community-scripts/ProxmoxVE/pull/18415))
- let post-pve-install go on when legacy sources are kept [@MickLesk](https://github.com/MickLesk) ([#18403](https://github.com/community-scripts/ProxmoxVE/pull/18403))
- #### 🔧 Refactor
- update-apps: say why an backup or restore failed [@MickLesk](https://github.com/MickLesk) ([#18405](https://github.com/community-scripts/ProxmoxVE/pull/18405))
- Keep host tools running when clear does not know the terminal [@MickLesk](https://github.com/MickLesk) ([#18412](https://github.com/community-scripts/ProxmoxVE/pull/18412))
- Refuse to run the PBS 4 upgrade on a Proxmox VE host [@MickLesk](https://github.com/MickLesk) ([#18416](https://github.com/community-scripts/ProxmoxVE/pull/18416))
- fstrim: keep trimming when pct fstrim fails on one container [@MickLesk](https://github.com/MickLesk) ([#18413](https://github.com/community-scripts/ProxmoxVE/pull/18413))
- execute-lxc: do not abort on a container that is not Debian or Ubuntu [@MickLesk](https://github.com/MickLesk) ([#18414](https://github.com/community-scripts/ProxmoxVE/pull/18414))
### 📚 Documentation
- github: flag instead of closing on first PR template miss; add script-request eligibility check [@MickLesk](https://github.com/MickLesk) ([#18384](https://github.com/community-scripts/ProxmoxVE/pull/18384))
## 2026-11-02
### 🚀 Updated Scripts
@@ -1208,189 +1475,4 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
- #### ✨ New Features
- FileBrowser Quantum: migrate the config and database to v2 [@MickLesk](https://github.com/MickLesk) ([#17815](https://github.com/community-scripts/ProxmoxVE/pull/17815))
## 2026-10-08
### 🆕 New Scripts
- Unifi-OS-Server VM ([#17752](https://github.com/community-scripts/ProxmoxVE/pull/17752))
- ZimaOS VM ([#17778](https://github.com/community-scripts/ProxmoxVE/pull/17778))
### 🚀 Updated Scripts
- Revert "Immich: Pin to v3.3.0" [@MickLesk](https://github.com/MickLesk) ([#17767](https://github.com/community-scripts/ProxmoxVE/pull/17767))
- #### 🐞 Bug Fixes
- Homarr: replace the musl better-sqlite3 that v2.3.0 ships for Debian [@MickLesk](https://github.com/MickLesk) ([#17789](https://github.com/community-scripts/ProxmoxVE/pull/17789))
- CLIProxyAPI: keep plugins and data across updates [@MickLesk](https://github.com/MickLesk) ([#17790](https://github.com/community-scripts/ProxmoxVE/pull/17790))
- Kima-Hub: install the Python services into a uv venv [@MickLesk](https://github.com/MickLesk) ([#17791](https://github.com/community-scripts/ProxmoxVE/pull/17791))
- #### ✨ New Features
- Immich: Pin to 3.3.0 / Update ML Python to 3.13 [@MickLesk](https://github.com/MickLesk) ([#17770](https://github.com/community-scripts/ProxmoxVE/pull/17770))
- #### 🔧 Refactor
- Refactor: OPNsense VM [@MickLesk](https://github.com/MickLesk) ([#17785](https://github.com/community-scripts/ProxmoxVE/pull/17785))
- Refactor: Nextcloud VM (Turnkey) [@MickLesk](https://github.com/MickLesk) ([#17787](https://github.com/community-scripts/ProxmoxVE/pull/17787))
- Refactor: OpenWrt VM [@MickLesk](https://github.com/MickLesk) ([#17774](https://github.com/community-scripts/ProxmoxVE/pull/17774))
- Refactor: Ubuntu VM [@MickLesk](https://github.com/MickLesk) ([#17776](https://github.com/community-scripts/ProxmoxVE/pull/17776))
### 💾 Core
- Tolerate a missing datacenter.cfg in vm_keyboard_default [@MickLesk](https://github.com/MickLesk) ([core#124](https://github.com/community-scripts/core/pull/124))
- Take the ISO storage from the disk pool when it can hold ISOs [@MickLesk](https://github.com/MickLesk) ([core#122](https://github.com/community-scripts/core/pull/122))
- Shared VM helpers: disk import, releases, checksums, first boot, IP by MAC [@MickLesk](https://github.com/MickLesk) ([core#117](https://github.com/community-scripts/core/pull/117))
- tools: forge truncated release list [@MickLesk](https://github.com/MickLesk) ([core#118](https://github.com/community-scripts/core/pull/118))
- Remember a kept cached image until the upstream changes [@MickLesk](https://github.com/MickLesk) ([core#119](https://github.com/community-scripts/core/pull/119))
- Choose the VM keyboard layout and carry the host's timezone into prepared images [@MickLesk](https://github.com/MickLesk) ([core#120](https://github.com/community-scripts/core/pull/120))
- VM's: run first-boot units without debconf dialogs [@MickLesk](https://github.com/MickLesk) ([core#121](https://github.com/community-scripts/core/pull/121))
## 2026-10-07
### 🆕 New Scripts
- Fedora VM ([#17747](https://github.com/community-scripts/ProxmoxVE/pull/17747))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- AudioMuse-AI: replace the venv on update without asking [@MickLesk](https://github.com/MickLesk) ([#17738](https://github.com/community-scripts/ProxmoxVE/pull/17738))
- paperclip: run as a dedicated non-root user so Claude Code can skip permissions [@samvandenbossche](https://github.com/samvandenbossche) ([#17707](https://github.com/community-scripts/ProxmoxVE/pull/17707))
- Thingsboard: update Java version from 17 to 25 [@CerberusStyle](https://github.com/CerberusStyle) ([#17733](https://github.com/community-scripts/ProxmoxVE/pull/17733))
- #### ✨ New Features
- Immich: Pin to v3.3.0 [@vhsdream](https://github.com/vhsdream) ([#17759](https://github.com/community-scripts/ProxmoxVE/pull/17759))
- #### 💥 Breaking Changes
- OpenCloud: bump to v8.1.0, rebuild search index on upgrade [@SimKaiLong](https://github.com/SimKaiLong) ([#17710](https://github.com/community-scripts/ProxmoxVE/pull/17710))
- #### 🔧 Refactor
- Pangolin: Unpin Release, stable enough, Bump to latest [@MickLesk](https://github.com/MickLesk) ([#17740](https://github.com/community-scripts/ProxmoxVE/pull/17740))
- Refactor: Archlinux-VM [@MickLesk](https://github.com/MickLesk) ([#17741](https://github.com/community-scripts/ProxmoxVE/pull/17741))
- Refactor: MikroTik RouterOS [@MickLesk](https://github.com/MickLesk) ([#17748](https://github.com/community-scripts/ProxmoxVE/pull/17748))
### 💾 Core
- Fix Fedora and BLS cloud image console setup [@MickLesk](https://github.com/MickLesk) ([core#116](https://github.com/community-scripts/core/pull/116))
- Keep setup_nodejs alive when the caller's directory is gone [@MickLesk](https://github.com/MickLesk) ([core#114](https://github.com/community-scripts/core/pull/114))
## 2026-10-06
### 🆕 New Scripts
- Pricebuddy ([#17708](https://github.com/community-scripts/ProxmoxVE/pull/17708))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Webtrees: stop serving data/ and the source folders directly [@MickLesk](https://github.com/MickLesk) ([#17724](https://github.com/community-scripts/ProxmoxVE/pull/17724))
### 💾 Core
- Incus: check the architecture on Incus hosts too [@MickLesk](https://github.com/MickLesk) ([core#113](https://github.com/community-scripts/core/pull/113))
- Incus: Pass the app's var_* settings into Incus containers [@MickLesk](https://github.com/MickLesk) ([core#112](https://github.com/community-scripts/core/pull/112))
- Incus: stub storage_content_check on Incus [@MickLesk](https://github.com/MickLesk) ([core#104](https://github.com/community-scripts/core/pull/104))
- Incus: map Proxmox template versions to Incus image names [@MickLesk](https://github.com/MickLesk) ([core#111](https://github.com/community-scripts/core/pull/111))
- Incus: read storage names from .vars files [@MickLesk](https://github.com/MickLesk) ([core#110](https://github.com/community-scripts/core/pull/110))
- Incus: show the spinner inside containers again [@MickLesk](https://github.com/MickLesk) ([core#109](https://github.com/community-scripts/core/pull/109))
- Incus: set the hostname with sh, so it works before bash is installed [@MickLesk](https://github.com/MickLesk) ([core#102](https://github.com/community-scripts/core/pull/102))
- Incus: reserve a plain address on Incus, leave the gateway to the network [@MickLesk](https://github.com/MickLesk) ([core#101](https://github.com/community-scripts/core/pull/101))
- Incus: Wait for the network on Alpine OS too [@MickLesk](https://github.com/MickLesk) ([core#105](https://github.com/community-scripts/core/pull/105))
- Incus: do not fail the build when hostname -I is missing [@MickLesk](https://github.com/MickLesk) ([core#103](https://github.com/community-scripts/core/pull/103))
- Incus: Fuse always in unprivileged Containers, attach TUN only when container lacks it [@MickLesk](https://github.com/MickLesk) ([core#108](https://github.com/community-scripts/core/pull/108))
- Incus: accept mode=generated [@MickLesk](https://github.com/MickLesk) ([core#106](https://github.com/community-scripts/core/pull/106))
- Incus: pass the full install environment into Incus containers (quoted) [@MickLesk](https://github.com/MickLesk) ([core#107](https://github.com/community-scripts/core/pull/107))
## 2026-10-05
### 🚀 Updated Scripts
- Point to DevScripts, the renamed ProxmoxVED [@MickLesk](https://github.com/MickLesk) ([#17694](https://github.com/community-scripts/ProxmoxVE/pull/17694))
- #### 🐞 Bug Fixes
- wanderer: set the proxy secret and install plugins in their own directories [@MickLesk](https://github.com/MickLesk) ([#17698](https://github.com/community-scripts/ProxmoxVE/pull/17698))
- discourse: serve stylesheets and repair the update [@MickLesk](https://github.com/MickLesk) ([#17697](https://github.com/community-scripts/ProxmoxVE/pull/17697))
- wikijs: bump Node.js from 24 to 26 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17689](https://github.com/community-scripts/ProxmoxVE/pull/17689))
- jotty: bump Node.js from 22 to 24 [@github-actions[bot]](https://github.com/github-actions[bot]) ([#17688](https://github.com/community-scripts/ProxmoxVE/pull/17688))
### 💾 Core
- Read releases from github.com when the API is rate limited, resume stalled downloads [@MickLesk](https://github.com/MickLesk) ([core#99](https://github.com/community-scripts/core/pull/99))
- Check for template and container storage before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#98](https://github.com/community-scripts/core/pull/98))
- Check /etc/pve before the settings menu [@MickLesk](https://github.com/MickLesk) ([core#97](https://github.com/community-scripts/core/pull/97))
- Check container settings before pct create rejects them [@MickLesk](https://github.com/MickLesk) ([core#96](https://github.com/community-scripts/core/pull/96))
- PVE: Smart Diagnosis - say why a container would not start [@MickLesk](https://github.com/MickLesk) ([core#95](https://github.com/community-scripts/core/pull/95))
- Retry template downloads that pveam reports as done but are not [@MickLesk](https://github.com/MickLesk) ([core#94](https://github.com/community-scripts/core/pull/94))
- Check the OS release against pve-container before creating anything [@MickLesk](https://github.com/MickLesk) ([core#93](https://github.com/community-scripts/core/pull/93))
- Rename "ProxmoxVED"-Links to "DevScripts" [@MickLesk](https://github.com/MickLesk) ([core#91](https://github.com/community-scripts/core/pull/91))
### 🧰 Tools
- #### 🐞 Bug Fixes
- monitor-all: read container IPs from the host side [@jasonobrien](https://github.com/jasonobrien) ([#17686](https://github.com/community-scripts/ProxmoxVE/pull/17686))
## 2026-10-04
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- immich: read the Intel runtime from the pinned release [@MickLesk](https://github.com/MickLesk) ([#17677](https://github.com/community-scripts/ProxmoxVE/pull/17677))
### 💾 Core
- better explain unsupported version & upgrade path for pve [@MickLesk](https://github.com/MickLesk) ([core#86](https://github.com/community-scripts/core/pull/86))
- Quote PostgreSQL identifiers and drop spaces from the creds file name [@MickLesk](https://github.com/MickLesk) ([core#89](https://github.com/community-scripts/core/pull/89))
- Find the default Rust toolchain in current rustup output [@MickLesk](https://github.com/MickLesk) ([core#88](https://github.com/community-scripts/core/pull/88))
## 2026-10-03
### 🆕 New Scripts
- Keeper.sh ([#17659](https://github.com/community-scripts/ProxmoxVE/pull/17659))
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- hermesagent: download the installer before running it [@MickLesk](https://github.com/MickLesk) ([#17666](https://github.com/community-scripts/ProxmoxVE/pull/17666))
- frigate: raise the Node heap for the web build [@MickLesk](https://github.com/MickLesk) ([#17667](https://github.com/community-scripts/ProxmoxVE/pull/17667))
- Tracearr: workaround for failing map tiles download and data preservation [@durzo](https://github.com/durzo) ([#17661](https://github.com/community-scripts/ProxmoxVE/pull/17661))
- fix(nginxproxymanager): clean yarn cache after builds to prevent disk growth [@PsycoStea](https://github.com/PsycoStea) ([#17653](https://github.com/community-scripts/ProxmoxVE/pull/17653))
- #### 🔧 Refactor
- Several Scripts: refactor msg_ blocks, generate passwords with helper, add CLEAN_INSTALL_KEEP [@MickLesk](https://github.com/MickLesk) ([#17655](https://github.com/community-scripts/ProxmoxVE/pull/17655))
### 💾 Core
- detect_repo_source: read the license header and fall back to ProxmoxVE [@MickLesk](https://github.com/MickLesk) ([core#85](https://github.com/community-scripts/core/pull/85))
## 2026-10-02
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- zerobyte: upgrade Bun on update [@MickLesk](https://github.com/MickLesk) ([#17646](https://github.com/community-scripts/ProxmoxVE/pull/17646))
- sure: keep Active Storage uploads across updates [@MickLesk](https://github.com/MickLesk) ([#17648](https://github.com/community-scripts/ProxmoxVE/pull/17648))
- twenty: run upstreams upgrade command instead of the removed setup-db.ts [@MickLesk](https://github.com/MickLesk) ([#17647](https://github.com/community-scripts/ProxmoxVE/pull/17647))
- #### 🔧 Refactor
- NPM: Refactor / Fix npm openresty user and certbot version [@MickLesk](https://github.com/MickLesk) ([#17649](https://github.com/community-scripts/ProxmoxVE/pull/17649))
- several scripts: use the release helpers instead of hand-rolled version checks and downloads [@MickLesk](https://github.com/MickLesk) ([#17625](https://github.com/community-scripts/ProxmoxVE/pull/17625))
### 💾 Core
- QoL: message blocks, clean-install keep list, password helper, network limits [@MickLesk](https://github.com/MickLesk) ([core#61](https://github.com/community-scripts/core/pull/61))
- FileBrowser Quantum: migrate the config and database to v2 [@MickLesk](https://github.com/MickLesk) ([#17815](https://github.com/community-scripts/ProxmoxVE/pull/17815))
+7 -3
View File
@@ -55,9 +55,13 @@ update_deb_based() {
setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "binary"
msg_info "Updating xCaddy"
$STD xcaddy build
msg_ok "Updated xCaddy"
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy updated; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Updating xCaddy"
$STD xcaddy build
msg_ok "Updated xCaddy"
fi
fi
fi
msg_ok "Updated successfully!"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/chattocorp/chatto
APP="Chatto"
var_tags="${var_tags:-chat;messaging}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/chatto ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "chatto" "chattocorp/chatto"; then
msg_info "Stopping Service"
systemctl stop chatto
msg_ok "Stopped Service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "chatto" "chattocorp/chatto" "prebuild" "latest" "/opt/chatto" "chatto_Linux_$(arch_resolve x86_64 arm64).tar.gz"
chmod +x /opt/chatto/chatto
msg_info "Starting Service"
systemctl start chatto
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:4000${CL}"
+2 -2
View File
@@ -36,7 +36,7 @@ function update_script() {
systemctl stop cinephage
msg_ok "Stopped Cinephage"
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
msg_info "Building Cinephage"
@@ -48,7 +48,7 @@ function update_script() {
msg_ok "Built Cinephage"
msg_info "Updating Camoufox"
$STD /opt/cinephage/node_modules/.bin/camoufox-js fetch
$STD /opt/cinephage/node_modules/.bin/camoufox fetch
msg_ok "Updated Camoufox"
msg_info "Starting Cinephage"
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://coolify.io/
APP="Coolify"
var_tags="${var_tags:-docker;paas}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-4096}"
var_disk="${var_disk:-30}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
ADDON_SCRIPT="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh"
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /data/coolify ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_warn "⚠️ ${APP} has been migrated to an addon script."
echo ""
echo -e "${INFO}${YW}This is a one-time migration. After this, you can update ${APP} anytime with:${CL}"
echo -e "${TAB}${TAB}${GN}update_coolify${CL} or ${GN}bash <(curl -fsSL ${ADDON_SCRIPT})${CL}"
echo ""
read -r -p "${TAB}Migrate update function now? [y/N]: " CONFIRM
if [[ ! "${CONFIRM,,}" =~ ^(y|yes)$ ]]; then
msg_warn "Migration skipped. The old update will continue to work for now."
msg_info "Updating ${APP} (legacy)"
$STD bash <(curl -fsSL https://cdn.coollabs.io/coolify/install.sh)
msg_ok "Updated ${APP}"
exit
fi
msg_info "Migrating update function"
TMP_UPDATE=$(mktemp)
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh)"
MIGRATION_EOF
mv "$TMP_UPDATE" /usr/bin/update
chmod +x /usr/bin/update
ln -sf /usr/bin/update /usr/bin/update_coolify 2>/dev/null || true
msg_ok "Migration complete"
echo -e "${INFO}${YW}Running addon update${CL}"
type=update bash <(curl -fsSL "${ADDON_SCRIPT}")
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
+1 -1
View File
@@ -26,7 +26,7 @@ function update_script() {
check_container_storage
check_container_resources
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
ensure_dependencies build-essential
if command -v cross-seed &>/dev/null; then
+10 -38
View File
@@ -38,57 +38,29 @@ function update_script() {
$STD systemctl stop databasus
msg_ok "Stopped Databasus"
create_backup /opt/databasus/.env
msg_info "Ensuring Database Clients"
# Create PostgreSQL version symlinks for compatibility
for v in 12 13 14 15 16 18; do
ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v
done
# Install MongoDB Database Tools via direct .deb (no APT repo for Debian 13)
if ! command -v mongodump &>/dev/null; then
[[ "$(get_os_info id)" == "ubuntu" ]] && MONGO_DIST="ubuntu2204" || MONGO_DIST="debian12"
MONGO_ARCH=$(arch_resolve "x86_64" "arm64")
# MongoDB only publishes arm64 builds for Ubuntu
[[ "$MONGO_ARCH" == "arm64" ]] && MONGO_DIST="ubuntu2204"
fetch_and_deploy_from_url "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-${MONGO_DIST}-${MONGO_ARCH}-100.16.1.deb"
fi
ensure_dependencies mariadb-client
mkdir -p /usr/local/mariadb-{10.6,12.1}/bin /usr/local/mysql-{5.7,8.0,8.4,9}/bin /usr/local/mongodb-database-tools/bin
[[ -f /usr/bin/mongodump ]] && ln -sf /usr/bin/mongodump /usr/local/mongodb-database-tools/bin/mongodump
[[ -f /usr/bin/mongorestore ]] && ln -sf /usr/bin/mongorestore /usr/local/mongodb-database-tools/bin/mongorestore
# Create MariaDB and MySQL client symlinks for compatibility
for dir in /usr/local/mariadb-{10.6,12.1}/bin; do
ln -sf /usr/bin/mariadb-dump "$dir/mariadb-dump"
ln -sf /usr/bin/mariadb "$dir/mariadb"
done
for dir in /usr/local/mysql-{5.7,8.0,8.4,9}/bin; do
ln -sf /usr/bin/mariadb-dump "$dir/mysqldump"
ln -sf /usr/bin/mariadb "$dir/mysql"
done
msg_ok "Ensured Database Clients"
ensure_dependencies libedit2 libgssapi-krb5-2 libncurses6 libreadline8t64
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus"
GO_VERSION="$(grep -m1 '^go ' /opt/databasus/backend/go.mod | awk '{print $2}')" setup_go
msg_info "Updating Databasus"
RELEASE=$(cat ~/.databasus)
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
cd /opt/databasus/frontend
$STD corepack prepare pnpm@latest --activate
$STD pnpm install --frozen-lockfile
$STD pnpm run build
$STD env VITE_APP_VERSION="${RELEASE}" pnpm run build
cd /opt/databasus/backend
$STD go mod download
$STD /root/go/bin/swag init -g cmd/main.go -o swagger
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$(arch_resolve) go build -o databasus ./cmd
mv /opt/databasus/backend/databasus /opt/databasus/databasus
$STD env CGO_ENABLED=0 GOOS=linux GOARCH="$(arch_resolve)" go build -o /opt/databasus/databasus ./cmd/databasus
mkdir -p /opt/databasus/agent-binaries
for arch in amd64 arm64; do
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -ldflags "-X main.Version=${RELEASE}" -o /opt/databasus/agent-binaries/databasus-verification-agent-linux-$arch ./cmd/verification-agent
done
mkdir -p /opt/databasus/ui/build
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
cp -r /opt/databasus/backend/migrations /opt/databasus/
sed -i '/^APP_VERSION=/d' /.env
echo "APP_VERSION=${RELEASE}" >>/.env
msg_ok "Updated Databasus"
restore_backup
chown -R postgres:postgres /opt/databasus
if ! grep -q "EnvironmentFile=/.env" /etc/systemd/system/databasus.service; then
+11
View File
@@ -58,6 +58,17 @@ function update_script() {
$STD git pull origin main
msg_ok "Updated Discourse"
if check_for_gh_release "libvips" "libvips/libvips"; then
ensure_dependencies meson ninja-build pkg-config libglib2.0-dev libexpat1-dev libjpeg-dev libpng-dev libwebp-dev libtiff-dev libheif-dev libjxl-dev librsvg2-dev libcgif-dev libexif-dev liblcms2-dev liborc-0.4-dev libimagequant-dev libpango1.0-dev
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "libvips" "libvips/libvips" "prebuild" "latest" "/opt/libvips" "vips-*.tar.xz"
msg_info "Building libvips"
cd /opt/libvips
$STD meson setup build --prefix=/usr/local --libdir=lib --buildtype=release -Dmodules=disabled -Dmagick=disabled -Dexamples=false
$STD ninja -C build install
ldconfig
msg_ok "Built libvips"
fi
msg_info "Updating Dependencies"
RUBY_TARGET=$(grep -A1 '^RUBY VERSION' /opt/discourse/Gemfile.lock | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
RUBY_VERSION="${RUBY_TARGET:-4.0.7}" RUBY_INSTALL_RAILS="false" setup_ruby
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Masked-Kunsiquat
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.espocrm.com/ | Github: https://github.com/espocrm/espocrm
APP="EspoCRM"
var_tags="${var_tags:-crm;business}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
var_arm64="${var_arm64:-yes}"
var_unprivileged="${var_unprivileged:-1}"
export var_admin_user="${var_admin_user:-}"
export var_admin_pass="${var_admin_pass:-}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/espocrm ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "espo-crm" "espocrm/espocrm"; then
msg_info "Stopping Services"
systemctl stop nginx cron php8.4-fpm
msg_ok "Stopped Services"
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data custom client/custom" fetch_and_deploy_gh_release "espo-crm" "espocrm/espocrm" "prebuild" "latest" "/opt/espocrm" "EspoCRM-*.zip"
msg_info "Migrating EspoCRM"
cd /opt/espocrm
$STD php bin/command clear-cache
if ! $STD php bin/command migrate; then
chown -R www-data:www-data /opt/espocrm
systemctl start php8.4-fpm
systemctl start cron nginx
msg_error "Migration failed; customizations may be incompatible. Check /opt/espocrm/data/logs, then run update again."
exit 1
fi
chown -R www-data:www-data /opt/espocrm
msg_ok "Migrated EspoCRM"
msg_info "Starting Services"
systemctl start php8.4-fpm
systemctl start cron nginx
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/mozilla-services/syncstorage-rs
APP="Firefox-Syncserver"
var_tags="${var_tags:-sync;firefox}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-3072}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-yes}" # built from source, nothing x86-only, but not yet run on arm64
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /usr/local/bin/syncserver ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs"; then
msg_info "Stopping Firefox Syncserver"
systemctl stop firefox-syncserver
msg_ok "Stopped Firefox Syncserver"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs" "tarball"
setup_rust
msg_info "Building Firefox Syncserver (Patience)"
cd /opt/firefox-syncserver
$STD cargo build --release --locked --no-default-features --features=syncstorage-db/postgres --features=tokenserver-db/postgres --features=py_verifier
cp target/release/syncserver /usr/local/bin/syncserver
rm -rf target
$STD uv venv --python /usr/bin/python3 /opt/firefox-syncserver/.venv
$STD uv pip install --python /opt/firefox-syncserver/.venv/bin/python pyfxa tokenlib cryptography
msg_ok "Built Firefox Syncserver"
msg_info "Starting Firefox Syncserver"
systemctl start firefox-syncserver
msg_ok "Started Firefox Syncserver"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}In Firefox, set identity.sync.tokenserver.uri in about:config to:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8000/1.0/sync/1.5${CL}"
+23 -6
View File
@@ -45,15 +45,32 @@ function update_script() {
export CGO_ENABLED=0
$STD go mod download
$STD go build -o /usr/local/bin/gotenberg \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v9/cmd.Version=$(cat ~/.gotenberg)'" \
cmd/gotenberg/main.go
msg_ok "Built gotenberg"
msg_info "Updating unoconverter"
UNOCONVERTER_VERSION=$(get_latest_github_release "gotenberg/unoconverter" "false")
download_file "https://raw.githubusercontent.com/gotenberg/unoconverter/${UNOCONVERTER_VERSION}/unoconv" /usr/local/bin/unoconverter
chmod +x /usr/local/bin/unoconverter
msg_ok "Updated unoconverter"
LOK_VERSION="$(awk -F= '/^ARG LOK_VERSION=/{print $2}' /opt/gotenberg/build/Dockerfile)"
fetch_and_deploy_gh_release "lok" "gotenberg/lok" "prebuild" "$LOK_VERSION" "/usr/local/bin" "lok_${LOK_VERSION#v}_linux_$(arch_resolve "amd64" "arm64").tar.gz"
msg_info "Migrating to Gotenberg 9"
CODENAME=$(get_os_info codename)
setup_deb822_repo \
"debian-backports" \
"file:///usr/share/keyrings/debian-archive-keyring.gpg" \
"http://deb.debian.org/debian" \
"${CODENAME}-backports" \
"main"
$STD apt install -y fonts-urw-base35
$STD apt install -y -t "${CODENAME}-backports" \
libreoffice-writer libreoffice-calc libreoffice-impress libreoffice-draw libreoffice-math
$STD apt purge -y python3-uno python-is-python3 pdftk-java qpdf libimage-exiftool-perl
$STD apt autoremove -y
rm -rf /usr/local/bin/unoconverter /usr/local/bin/pdfcpu /opt/pdfcpu
sed -i '/^Environment=\(UNOCONVERTER\|PDFTK\|QPDF\|EXIFTOOL\|PDFCPU\)_BIN_PATH=/d' /etc/systemd/system/gotenberg.service
grep -q '^Environment=LOK_BIN_PATH=' /etc/systemd/system/gotenberg.service ||
sed -i '/^Environment=LIBREOFFICE_BIN_PATH=/a Environment=LOK_BIN_PATH=/usr/local/bin/lok' /etc/systemd/system/gotenberg.service
systemctl daemon-reload
msg_ok "Migrated to Gotenberg 9"
msg_info "Starting Service"
systemctl start gotenberg
+2 -27
View File
@@ -44,7 +44,7 @@ function update_script() {
docker pull "${CONTAINER_IMAGE}"
LATEST_IMAGE="$(docker inspect --format "{{.Id}}" --type image "${CONTAINER_IMAGE}")"
if [[ "${RUNNING_IMAGE}" != "${LATEST_IMAGE}" ]]; then
pip install -U runlike
pip install -U runlike --break-system-packages
echo "Updating ${container} image ${CONTAINER_IMAGE}"
DOCKER_COMMAND="$(runlike --use-volume-id "${container}")"
docker rm --force "${container}"
@@ -70,32 +70,7 @@ function update_script() {
exit
fi
if [ "$UPD" == "4" ]; then
msg_info "Installing FileBrowser"
RELEASE=$(curl -fsSL https://api.github.com/repos/filebrowser/filebrowser/releases/latest | grep -o '"tag_name": ".*"' | sed 's/"//g' | sed 's/tag_name: //g')
$STD curl -fsSL https://github.com/filebrowser/filebrowser/releases/download/v2.23.0/linux-$(arch_resolve)-filebrowser.tar.gz | tar -xzv -C /usr/local/bin
$STD filebrowser config init -a '0.0.0.0'
$STD filebrowser config set -a '0.0.0.0'
$STD filebrowser users add admin community-scripts.org --perm.admin
msg_ok "Installed FileBrowser"
msg_info "Creating Service"
service_path="/etc/systemd/system/filebrowser.service"
echo "[Unit]
Description=Filebrowser
After=network-online.target
[Service]
User=root
WorkingDirectory=/root/
ExecStart=/usr/local/bin/filebrowser -r /
[Install]
WantedBy=default.target" >$service_path
$STD systemctl enable --now filebrowser
msg_ok "Created Service"
msg_ok "Completed successfully!\n"
echo -e "FileBrowser should be reachable by going to the following URL.
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
exit
fi
}
+44 -20
View File
@@ -75,7 +75,7 @@ EOF
BASE_DIR=${STAGING_DIR}/base-images
SOURCE_DIR=${STAGING_DIR}/image-source
cd /tmp
RELEASE="v3.3.5"
RELEASE="v3.4.1"
if [[ -f ~/.intel_version ]]; then
INTEL_SRC="https://raw.githubusercontent.com/immich-app/immich/${RELEASE}/machine-learning"
curl -fsSL "${INTEL_SRC}/scripts/install-intel-runtime.sh" -o ./intel-runtime 2>/dev/null ||
@@ -109,7 +109,7 @@ EOF
libraries=("libjxl" "jpegli" "libheif" "libraw" "imagemagick" "libvips")
cd "$BASE_DIR"
msg_warn "Checking for updates to custom image-processing libraries (recompile time: 2-15min per library)"
ensure_dependencies liblcms2-dev libjpeg62-turbo-dev libspng-dev libexif-dev libopenjp2-7-dev
ensure_dependencies liblcms2-dev libjpeg62-turbo-dev libspng-dev libexif-dev libopenjp2-7-dev pigz
$STD git pull
for library in "${libraries[@]}"; do
compile_"$library"
@@ -131,7 +131,18 @@ EOF
if check_for_gh_release "Immich" "immich-app/immich" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
msg_warn "The update takes 5-15 minutes, do not interrupt it"
PREV_IMMICH="$(cat ~/.immich)"
PREV_IMMICH="${CS_PREVIOUS_VERSION:-$(cat ~/.immich)}"
INSTALL_DIR="/opt/${APP}"
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
SRC_DIR="${INSTALL_DIR}/source"
APP_DIR="${INSTALL_DIR}/app"
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
setup_uv
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
# An interrupted update leaves app/ empty, so there may be no immich-admin to call.
if [[ "$PREV_IMMICH" > "2.5.1" && -x /opt/immich/app/bin/immich-admin ]]; then
msg_info "Enabling Maintenance Mode"
@@ -141,6 +152,7 @@ EOF
$STD cd -
msg_ok "Enabled Maintenance Mode"
fi
[[ -x /opt/immich/app/bin/immich-admin ]] || export MAINT_MODE=1
msg_info "Stopping Services"
systemctl stop immich-web
systemctl stop immich-ml
@@ -149,13 +161,13 @@ EOF
PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -1)
PG_VERSION=${PG_VERSION:-16}
[[ -f ~/.vchord_version ]] && mv ~/.vchord_version ~/.vectorchord
if check_for_gh_release "VectorChord" "tensorchord/VectorChord" "${VCHORD_RELEASE}" "updated together with Immich after testing"; then
if check_for_gh_release "VectorChord" "supervc-stack/VectorChord" "${VCHORD_RELEASE}" "updated together with Immich after testing"; then
# dead tuples in smart_search/face_search make the REINDEX below fail with
# "missing chunk ... for toast value" on VectorChord 1.0.0 (#15588); must vacuum
# while still on the old extension version, a post-upgrade vacuum errors instead
$STD sudo -u postgres psql -d immich -c "VACUUM (ANALYZE) smart_search;"
$STD sudo -u postgres psql -d immich -c "VACUUM (ANALYZE) face_search;"
fetch_and_deploy_gh_release "VectorChord" "tensorchord/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${PG_VERSION}-vchord_*_$(arch_resolve).deb"
fetch_and_deploy_gh_release "VectorChord" "supervc-stack/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${PG_VERSION}-vchord_*_$(arch_resolve).deb"
systemctl restart postgresql
$STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vector UPDATE;"
$STD sudo -u postgres psql -d immich -c "ALTER EXTENSION vchord UPDATE;"
@@ -164,14 +176,6 @@ EOF
fi
ensure_dependencies ccache gcc-13 g++-13
INSTALL_DIR="/opt/${APP}"
UPLOAD_DIR="$(sed -n '/^IMMICH_MEDIA_LOCATION/s/[^=]*=//p' /opt/immich/.env)"
SRC_DIR="${INSTALL_DIR}/source"
APP_DIR="${INSTALL_DIR}/app"
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
[[ -f "$ML_DIR"/ml_start.sh ]] && cp "$ML_DIR"/ml_start.sh "$INSTALL_DIR"
if grep -qs "set -a" "$APP_DIR"/bin/start.sh && grep -qs "warnings" "$APP_DIR"/bin/start.sh; then
cp "$APP_DIR"/bin/start.sh "$INSTALL_DIR"
@@ -193,8 +197,6 @@ EOF
rm -rf "${APP_DIR:?}"/*
)
setup_uv
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
export CI=1
@@ -232,7 +234,7 @@ EOF
# plugins
cd "$SRC_DIR"
export MISE_TRUSTED_CONFIG_PATHS="$SRC_DIR"/mise.toml
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,restic,npm:@openapitools/openapi-generator-cli
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
mise_ok=0
for i in 1 2 3; do
@@ -249,7 +251,8 @@ EOF
# extism-js ships as a bare gzip-compressed single binary (.gz) that
# fetch_and_deploy_gh_release cannot deploy; fetch + gunzip it directly.
EXTISM_ARCH="$(arch_resolve x86_64 aarch64)"
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-${EXTISM_ARCH}-linux-v1.6.0.gz"
EXTISM_VERSION="$(grep -oP '"github:extism/js-pdk"\s*=\s*"\K[^"]+' "$SRC_DIR"/mise.toml)"
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/${EXTISM_VERSION}/extism-js-${EXTISM_ARCH}-linux-${EXTISM_VERSION}.gz"
gunzip -f /tmp/extism-js.gz
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
rm -f /tmp/extism-js
@@ -274,6 +277,27 @@ EOF
cp ./packages/plugin-core/manifest.json "$PLUGIN_DIR"
msg_ok "Updated Immich server, web, cli and plugins"
RESTIC_REF="$(grep -oP 'futo-org/restic:sha-\K[0-9a-f]+' "$SRC_DIR"/server/Dockerfile)"
RESTIC_PROXY_REF="$(grep -oP 'futo-org/backups/restic-proxy:\K[^@]+' "$SRC_DIR"/server/Dockerfile)"
if [[ "$(cat ~/.immich_restic 2>/dev/null)" != "${RESTIC_REF} ${RESTIC_PROXY_REF}" || ! -x /usr/local/bin/restic ]]; then
FUTO_DIR=/opt/staging/futo
rm -rf "$FUTO_DIR"
$STD git clone --filter=blob:none --no-checkout https://github.com/futo-org/restic "$FUTO_DIR"/restic
$STD git -C "$FUTO_DIR"/restic checkout "$RESTIC_REF"
$STD git clone --depth 1 --filter=blob:none --sparse -b "$RESTIC_PROXY_REF" https://github.com/futo-org/backups "$FUTO_DIR"/backups
$STD git -C "$FUTO_DIR"/backups sparse-checkout set packages/restic-proxy
GO_VERSION="$(awk '/^go /{print $2}' "$FUTO_DIR"/backups/packages/restic-proxy/go.mod)" setup_go
msg_info "Building restic for FUTO Backups"
cd "$FUTO_DIR"/restic
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic ./cmd/restic
cd "$FUTO_DIR"/backups/packages/restic-proxy
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic-proxy .
cd "$SRC_DIR"
rm -rf "$FUTO_DIR"
echo "${RESTIC_REF} ${RESTIC_PROXY_REF}" >~/.immich_restic
msg_ok "Built restic for FUTO Backups"
fi
cd "$SRC_DIR"/machine-learning
mkdir -p "$ML_DIR"
# chown excluding upload dir contents (may be a mount with restricted permissions)
@@ -292,7 +316,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -310,7 +334,7 @@ EOF
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Updating machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { ML_FAILED=1; break; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -419,7 +443,7 @@ function compile_libjxl() {
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
cd "$SOURCE"
$STD git reset --hard "$LIBJXL_REVISION"
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
$STD git submodule update --init --recursive --depth 1 --recommend-shallow -- third_party
mkdir build
cd build
$STD cmake \
+2 -2
View File
@@ -32,14 +32,14 @@ function update_script() {
fi
if check_for_gh_tag "kan" "kanbn/kan"; then
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
msg_info "Stopping Service"
systemctl stop kan
msg_ok "Stopped Service"
create_backup /opt/kan/.env
CLEAN_INSTALL=1 fetch_and_deploy_gh_tag "kan" "kanbn/kan" "latest"
restore_backup
msg_info "Building Application"
+1 -1
View File
@@ -36,7 +36,7 @@ function update_script() {
source ~/.bashrc
if ! command -v deno &>/dev/null; then
export DENO_INSTALL="/usr/local"
curl -fsSL https://deno.land/install.sh | $STD sh -s -- -y
curl -fsSL https://deno.land/install.sh | CI=1 $STD sh -s
else
$STD deno upgrade
fi
+2 -2
View File
@@ -9,7 +9,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
APP="MyIP"
var_tags="${var_tags:-network}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-1024}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-4}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
@@ -30,7 +30,7 @@ function update_script() {
exit
fi
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
if check_for_gh_release "myip" "jason5ng32/MyIP"; then
msg_info "Stopping Services"
+4 -1
View File
@@ -46,7 +46,10 @@ EOF
systemctl daemon-reload
fi
$STD npm install -g n8n@latest
# n8n-nodes-base 2.46.0 pins both @napi-rs/canvas musl binaries as hard
# dependencies, which no os/cpu/libc combination can satisfy. Drop --force
# once upstream makes them optional again.
$STD npm install -g n8n@latest --force
systemctl restart n8n
msg_ok "Updated n8n"
msg_ok "Updated successfully!"
+3
View File
@@ -31,6 +31,8 @@ function update_script() {
exit
fi
echo "x /tmp/nexterm-sea-*" >/etc/tmpfiles.d/nexterm.conf
if check_for_gh_release "nexterm-engine" "gnmyt/Nexterm"; then
msg_info "Stopping nexterm-engine"
systemctl stop nexterm-engine
@@ -46,6 +48,7 @@ function update_script() {
if check_for_gh_release "nexterm-server" "gnmyt/Nexterm"; then
msg_info "Stopping nexterm-server"
systemctl stop nexterm-server
rm -rf /tmp/nexterm-sea-*
msg_ok "Stopped nexterm-server"
fetch_and_deploy_gh_release "nexterm-server" "gnmyt/Nexterm" "singlefile" "latest" "/opt/nexterm/server" "nexterm-server-linux-$(arch_resolve "x64" "arm64")"
+6 -1
View File
@@ -32,9 +32,14 @@ function update_script() {
if check_for_gh_release "openobserve" "openobserve/openobserve"; then
msg_info "Updating OpenObserve"
if grep -q "^ExecStop=killall" /etc/systemd/system/openobserve.service; then
sed -i "/^ExecStop=killall/d" /etc/systemd/system/openobserve.service
systemctl daemon-reload
fi
systemctl stop openobserve
RELEASE=$(get_latest_github_release "openobserve/openobserve")
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve --no-same-owner
echo "${RELEASE}" >~/.openobserve
systemctl start openobserve
msg_ok "Updated OpenObserve"
msg_ok "Updated successfully!"
+41 -25
View File
@@ -32,36 +32,52 @@ function update_script() {
if check_for_gh_release "paperless-ai" "clusterzx/paperless-ai"; then
msg_info "Stopping Service"
systemctl stop paperless-ai paperless-rag
systemctl stop paperless-ai
if [[ -f /etc/systemd/system/paperless-rag.service ]]; then
systemctl stop paperless-rag
fi
msg_ok "Stopped Service"
msg_info "Backing up data"
cp -r /opt/paperless-ai/data /opt/paperless-ai-data-backup
msg_ok "Backed up data"
fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
msg_info "Restoring data"
cp -r /opt/paperless-ai-data-backup/* /opt/paperless-ai/data/
rm -rf /opt/paperless-ai-data-backup
msg_ok "Restored data"
NODE_VERSION="22" setup_nodejs
CLEAN_INSTALL=1 CLEAN_INSTALL_KEEP="data" fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
msg_info "Updating Paperless-AI"
cd /opt/paperless-ai
if [[ ! -d /opt/paperless-ai/venv ]]; then
msg_info "Recreating Python venv"
$STD python3 -m venv /opt/paperless-ai/venv
fi
source /opt/paperless-ai/venv/bin/activate
$STD pip install --upgrade pip
$STD pip install --no-cache-dir -r requirements.txt
mkdir -p data/chromadb
$STD npm ci --only=production
export ONNXRUNTIME_NODE_INSTALL=skip PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
$STD npm ci --ignore-scripts --no-audit --no-fund
$STD npm run build
$STD npm prune --omit=dev --ignore-scripts --no-audit --no-fund
msg_ok "Updated Paperless-AI"
if ! grep -q "dist/server/index.js" /etc/systemd/system/paperless-ai.service; then
msg_info "Migrating services to Paperless-AI 4"
if [[ -f /etc/systemd/system/paperless-rag.service ]]; then
systemctl disable -q paperless-rag
rm -f /etc/systemd/system/paperless-rag.service
fi
cat <<EOF >/etc/systemd/system/paperless-ai.service
[Unit]
Description=PaperlessAI Service
After=network.target
[Service]
WorkingDirectory=/opt/paperless-ai
Environment="NODE_ENV=production"
Environment="PAPERLESS_AI_PORT=3000"
Environment="PAPERLESS_AI_DATA_DIR=/opt/paperless-ai/data"
Environment="HF_HUB_DISABLE_TELEMETRY=1"
ExecStart=/usr/bin/node dist/server/index.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
msg_ok "Migrated services to Paperless-AI 4"
msg_warn "Settings and history from 3.x are migrated on first start. If this instance was reachable from the internet, read 'Upgrading from 3.x' in the upstream README first."
fi
msg_info "Starting Service"
systemctl start paperless-rag
sleep 3
systemctl start paperless-ai
msg_ok "Started Service"
msg_ok "Updated successfully!"
@@ -73,7 +89,7 @@ start
build_container
description
msg_ok "Completed successfully!\n"
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:3000${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://pixelfed.org/
APP="Pixelfed"
var_tags="${var_tags:-fediverse;social}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-10}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
# var_arm64="${var_arm64:-no}"
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -d /opt/pixelfed ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "pixelfed" "pixelfed/pixelfed"; then
msg_info "Stopping Services"
systemctl stop pixelfed-horizon pixelfed-scheduler.timer
msg_ok "Services stopped"
create_backup /opt/pixelfed/.env \
/opt/pixelfed/storage
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "pixelfed" "pixelfed/pixelfed" "tarball" "latest" "/opt/pixelfed"
restore_backup
msg_info "Updating Pixelfed"
cd /opt/pixelfed
chmod -R 755 /opt/pixelfed
chmod -R 775 /opt/pixelfed/storage /opt/pixelfed/bootstrap/cache
export COMPOSER_ALLOW_SUPERUSER=1
$STD composer install --no-dev --no-ansi --no-interaction --optimize-autoloader
chown -R pixelfed:pixelfed /opt/pixelfed
$STD sudo -u pixelfed php artisan storage:link
$STD sudo -u pixelfed php artisan migrate --force
$STD sudo -u pixelfed php artisan route:cache
$STD sudo -u pixelfed php artisan view:cache
$STD sudo -u pixelfed php artisan config:cache
$STD sudo -u pixelfed php artisan horizon:publish
msg_ok "Updated Pixelfed"
msg_info "Starting Services"
systemctl start pixelfed-horizon pixelfed-scheduler.timer
msg_ok "Services started"
msg_ok "Updated successfully!"
fi
exit
}
start
build_container
description
msg_ok "Completed successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}${CL}"
echo -e "${INFO}${YW} Create an admin account with:${CL}"
echo -e "${TAB}cd /opt/pixelfed && sudo -u pixelfed php artisan user:create"
+9
View File
@@ -30,6 +30,15 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
if ! grep -q "^Environment=HOME=" /etc/systemd/system/planka.service; then
msg_info "Updating Service"
sed -i -e '/^Environment=NODE_ENV=/d' -e 's|^ExecStart=.*|Environment=NODE_ENV=production\nEnvironment=HOME=/root\nExecStart=/usr/bin/node app.js --prod|' /etc/systemd/system/planka.service
systemctl daemon-reload
systemctl restart planka
msg_ok "Updated Service"
fi
if check_for_gh_release "planka" "plankanban/planka"; then
msg_info "Stopping Service"
systemctl stop planka
+7 -6
View File
@@ -4,7 +4,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
# Copyright (c) 2021-2026 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
# Source: https://pocketbase.io/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
APP="Pocketbase"
var_tags="${var_tags:-database}"
@@ -29,20 +29,21 @@ function update_script() {
msg_error "No ${APP} Installation Found!"
exit
fi
if check_for_gh_release "pocketbase" "pocketbase/pocketbase"; then
RELEASE=$(curl_with_retry "https://ganigeorgiev.com/api/collections/releases/records?filter=channel.repository.name%3D%22pocketbase%22%20%26%26%20channel.name%3D%22default%22&sort=-semver&skipTotal=1&perPage=1" "-" | jq -r '.items[0].tag' | sed 's/^v//')
if [[ -n "$RELEASE" && "$RELEASE" != "$(cat ~/.pocketbase 2>/dev/null)" ]]; then
msg_info "Stopping Service"
systemctl stop pocketbase
msg_ok "Stopped Service"
msg_info "Updating ${APP}"
/opt/pocketbase/pocketbase update
echo "${CHECK_UPDATE_RELEASE}" >~/.pocketbase
msg_ok "Updated ${APP}"
fetch_and_deploy_from_url "https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v${RELEASE}/pocketbase_${RELEASE}_linux_$(arch_resolve).zip" "/opt/pocketbase"
echo "${RELEASE}" >~/.pocketbase
msg_info "Starting Service"
systemctl start pocketbase
msg_ok "Started Service"
msg_ok "Updated successfully!"
else
msg_ok "No update required. ${APP} is already at v${RELEASE}"
fi
exit
}
+1 -27
View File
@@ -66,33 +66,7 @@ function update_script() {
exit
fi
if [ "$UPD" == "3" ]; then
msg_info "Installing FileBrowser"
$STD curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
$STD filebrowser config init -a '0.0.0.0'
$STD filebrowser config set -a '0.0.0.0'
$STD filebrowser users add admin community-scripts.org --perm.admin
msg_ok "Installed FileBrowser"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/filebrowser.service
[Unit]
Description=Filebrowser
After=network-online.target
[Service]
User=root
WorkingDirectory=/root/
ExecStart=/usr/local/bin/filebrowser -r /
[Install]
WantedBy=default.target
EOF
systemctl enable -q --now filebrowser
msg_ok "Created Service"
msg_ok "Completed successfully!\n"
echo -e "FileBrowser should be reachable by going to the following URL.
${BL}http://$LOCAL_IP:8080${CL} admin|community-scripts.org\n"
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/filebrowser.sh)"
exit
fi
if [ "$UPD" == "4" ]; then
+21 -9
View File
@@ -107,8 +107,9 @@ function update_script() {
ln -sfn "$ROMM_BASE"/resources /opt/romm/frontend/dist/assets/romm/resources
ln -sfn "$ROMM_BASE"/assets /opt/romm/frontend/dist/assets/romm/assets
if [[ -f /etc/angie/http.d/romm.conf ]]; then
if ! grep -q "js_content decode.decodeBase64" /etc/angie/http.d/romm.conf; then
msg_info "Adding missing /decode and /cache locations to Angie config"
if ! grep -q "js_content decode.decodeBase64" /etc/angie/http.d/romm.conf ||
! grep -q "coep_header" /etc/angie/http.d/romm.conf; then
msg_info "Updating Angie config (decode/cache locations, player isolation headers)"
dpkg -l angie-module-njs &>/dev/null || $STD apt install -y angie-module-njs
grep -q "ngx_http_js_module.so" /etc/angie/angie.conf || sed -i '1i load_module modules/ngx_http_js_module.so;' /etc/angie/angie.conf
mkdir -p /etc/angie/js "${ROMM_BASE}/cache"
@@ -147,6 +148,20 @@ map \$http_upgrade \$connection_upgrade {
'' close;
}
# COEP/COOP for the EmulatorJS and js-dos players (SharedArrayBuffer needs a
# crossOriginIsolated document). Matched on \$request_uri because try_files
# rewrites \$uri to /index.html before add_header runs, so a per-location
# add_header on the player route never fires. Mirrors upstream RomM's
# docker/nginx/templates/default.conf.template.
map \$request_uri \$coep_header {
default "";
~^/rom/.*/(ejs|jsdos)(\?|\$) "require-corp";
}
map \$request_uri \$coop_header {
default "";
~^/rom/.*/(ejs|jsdos)(\?|\$) "same-origin";
}
server {
listen 80;
server_name _;
@@ -155,6 +170,9 @@ server {
location / {
try_files \$uri \$uri/ /index.html;
add_header Cache-Control "no-cache";
add_header Cross-Origin-Embedder-Policy \$coep_header;
add_header Cross-Origin-Opener-Policy \$coop_header;
}
location /assets {
@@ -164,12 +182,6 @@ server {
add_header Cache-Control "public, immutable";
}
location ~ ^/rom/.*/ejs\$ {
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Opener-Policy "same-origin";
try_files \$uri /index.html;
}
location /api {
proxy_pass http://romm_backend;
proxy_buffering off;
@@ -209,7 +221,7 @@ server {
}
}
EOF
msg_ok "Added /decode and /cache locations"
msg_ok "Updated Angie config"
else
sed -i -e "s|alias .*/library/;|alias ${ROMM_BASE}/library/;|" \
-e "s|alias .*/cache/;|alias ${ROMM_BASE}/cache/;|" /etc/angie/http.d/romm.conf
+10
View File
@@ -32,6 +32,16 @@ function update_script() {
exit
fi
ensure_dependencies redis-server
systemctl enable -q --now redis-server
if ! grep -q "BACKEND_CACHE_REDIS_URI" /opt/teable/.env; then
sed -i '/^BACKEND_CACHE_SQLITE_URI=/d' /opt/teable/.env
sed -i 's|^BACKEND_CACHE_PROVIDER=.*|BACKEND_CACHE_PROVIDER=redis\nBACKEND_CACHE_REDIS_URI=redis://127.0.0.1:6379/0|' /opt/teable/.env
sed -i 's|^After=network.target postgresql.service$|After=network.target postgresql.service redis-server.service|' /etc/systemd/system/teable.service
systemctl daemon-reload
systemctl restart teable
fi
if check_for_gh_release "teable" "teableio/teable"; then
msg_info "Stopping Service"
systemctl stop teable
+1 -1
View File
@@ -47,7 +47,7 @@ function update_script() {
$STD apt install -y aspnetcore-runtime-10.0
fi
RELEASE=$(curl -fsSL https://technitium.com/dns/ | grep -oP 'Version \K[\d.]+')
RELEASE=$(get_latest_github_release "TechnitiumSoftware/DnsServer")
if [[ ! -f ~/.technitium || ${RELEASE} != "$(cat ~/.technitium 2>/dev/null)" ]]; then
fetch_and_deploy_from_url "https://download.technitium.com/dns/DnsServerPortable.tar.gz" /opt/technitium/dns
echo "${RELEASE}" >~/.technitium
+19 -1
View File
@@ -31,12 +31,30 @@ function update_script() {
fi
JAVA_VERSION="25" setup_java
if ! grep -q "install.data_dir" /etc/thingsboard/conf/thingsboard.conf; then
msg_info "Restoring Package Defaults"
cat <<'EOF' >>/etc/thingsboard/conf/thingsboard.conf
export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data"
export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/gc.log:time,uptime,level,tags:filecount=10,filesize=10M"
export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError"
export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark"
export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10"
export LOG_FILENAME=thingsboard.out
export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions
export SQL_DATA_FOLDER=/usr/share/thingsboard/data/sql
EOF
systemctl restart thingsboard
msg_ok "Restored Package Defaults"
fi
if check_for_gh_release "thingsboard" "thingsboard/thingsboard"; then
msg_info "Stopping Service"
systemctl stop thingsboard
msg_ok "Stopped Service"
fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
DPKG_FORCE_CONFOLD=1 fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
msg_info "Running Database Upgrade"
$STD /usr/share/thingsboard/bin/install/upgrade.sh
+14 -2
View File
@@ -32,7 +32,14 @@ function update_script() {
NODE_VERSION="22" setup_nodejs
if check_for_gh_release "tududi" "chrisvel/tududi"; then
RELEASE=""
if ! grep -qs '^DATABASE_URL=' /opt/tududi/backend/.env; then
RELEASE="v1.15.0"
msg_warn "Tududi 2.0 runs on PostgreSQL only, so this SQLite install stays on 1.15.0"
msg_warn "To move to 2.0, export in Settings > Backups and import into a new Tududi LXC"
fi
if check_for_gh_release "tududi" "chrisvel/tududi" "${RELEASE}" "2.0 needs PostgreSQL"; then
msg_info "Stopping Service"
systemctl stop tududi
msg_ok "Stopped Service"
@@ -43,9 +50,14 @@ function update_script() {
else
cp /opt/tududi/.env /opt/tududi.env
fi
if ! grep -q '^TUDUDI_UPLOAD_PATH=' /opt/tududi.env; then
mkdir -p /opt/tududi-uploads
[[ -d /opt/tududi/backend/uploads ]] && cp -a /opt/tududi/backend/uploads/. /opt/tududi-uploads/
echo "TUDUDI_UPLOAD_PATH=/opt/tududi-uploads" >>/opt/tududi.env
fi
msg_ok "Backed up env file"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tududi" "chrisvel/tududi" "tarball" "latest" "/opt/tududi"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tududi" "chrisvel/tududi" "tarball" "${CHECK_UPDATE_RELEASE}" "/opt/tududi"
msg_info "Updating Tududi"
cd /opt/tududi
+3 -6
View File
@@ -37,8 +37,6 @@ function update_script() {
create_backup /root/.local/share/tunarr
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "tunarr" "chrisbenincasa/tunarr" "prebuild" "latest" "/opt/tunarr" "*linux-$(arch_resolve "x64" "arm64").tar.gz"
cd /opt/tunarr
mv tunarr* tunarr
restore_backup
msg_info "Starting Service"
@@ -52,13 +50,12 @@ function update_script() {
systemctl stop tunarr
msg_ok "Stopped Service"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-7.1.tar.xz"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "ersatztv-ffmpeg" "ErsatzTV/ErsatzTV-ffmpeg" "prebuild" "latest" "/opt/ErsatzTV-ffmpeg" "*-linux$(arch_resolve "64" "arm64")-gpl-*.tar.xz"
msg_info "Set ErsatzTV-ffmpeg links"
chmod +x /opt/ErsatzTV-ffmpeg/bin/*
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffmpeg /usr/local/bin/ffmpeg
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffplay /usr/local/bin/ffplay
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffprobe /usr/local/bin/ffprobe
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffmpeg /usr/bin/ffmpeg
ln -sf /opt/ErsatzTV-ffmpeg/bin/ffprobe /usr/bin/ffprobe
msg_ok "ffmpeg links set"
msg_info "Starting Service"
+1
View File
@@ -56,3 +56,4 @@ msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:6969${CL}"
echo -e "${INFO}${YW} Login: whisparr, password in ~/whisparr-eros.creds inside the container${CL}"
+35
View File
@@ -30,6 +30,41 @@ function update_script() {
exit
fi
if [[ -d /opt/wikijs/backend ]]; then
NODE_VERSION="26" setup_nodejs
if check_for_gh_release "wikijs" "requarks/wiki"; then
PUPPETEER_INSTALLED=$([[ -d /opt/wikijs/backend/node_modules/puppeteer ]] && echo "true" || echo "false")
msg_info "Stopping Service"
systemctl stop wikijs
msg_ok "Stopped Service"
create_backup /opt/wikijs/config.yml
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "wikijs" "requarks/wiki" "prebuild" "latest" "/opt/wikijs" "wiki-js.tar.gz"
restore_backup
cd /opt/wikijs/backend
if [[ "$(arch_resolve)" == "arm64" ]]; then
msg_info "Building Wiki.js Dependencies"
$STD npm ci --omit=dev
msg_ok "Built Wiki.js Dependencies"
fi
if [[ "$PUPPETEER_INSTALLED" == "true" ]]; then
msg_info "Reinstalling Puppeteer"
$STD npm install --no-save --force --include=optional --no-ignore-scripts --no-audit --no-fund "puppeteer@$(sed -n 's/^installVersion: *//p' modules/extensions/puppeteer/definition.yml)"
msg_ok "Reinstalled Puppeteer"
fi
msg_info "Starting Service"
systemctl start wikijs
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
exit
fi
msg_warn "Wiki.js 3 cannot update a 2.x install in place. Export it under Administration > Utilities > Export for Wiki.js 3.x and import it into a new Wiki.js 3 container."
NODE_VERSION="26" NODE_MODULE="yarn,node-gyp" setup_nodejs
if check_for_gh_release "wikijs" "requarks/wiki-v2"; then
+1 -1
View File
@@ -22,7 +22,7 @@ if [[ "${var_os:-}" == "alpine" ]]; then
var_disk="${var_disk:-1}"
var_version="${var_version:-3.24}"
else
var_ram="${var_ram:-512}"
var_ram="${var_ram:-1024}"
var_disk="${var_disk:-4}"
var_version="${var_version:-13}"
fi
+1 -1
View File
@@ -45,7 +45,7 @@ function update_script() {
msg_info "Running Post-Update Tasks"
cd /opt/writefreely
$STD ./writefreely db migrate
ln -s /opt/writefreely/writefreely /usr/local/bin/writefreely
ln -sf /opt/writefreely/writefreely /usr/local/bin/writefreely
msg_ok "Ran Post-Update Tasks"
msg_info "Starting Services"
+1 -1
View File
@@ -38,7 +38,7 @@ function update_script() {
create_backup /opt/yuvomi/data /opt/yuvomi/.env
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "tarball"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yuvomi" "ulsklyc/yuvomi" "prebuild" "latest" "/opt/yuvomi" "yuvomi-*-dist.tar.gz"
NODE_VERSION="24" setup_nodejs
msg_info "Installing Node.js Dependencies"
+1 -1
View File
@@ -9,7 +9,7 @@ source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_
APP="Zipline"
var_tags="${var_tags:-file;sharing}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_ram="${var_ram:-3072}"
var_disk="${var_disk:-5}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
+3
View File
@@ -20,6 +20,9 @@ $STD apt install -y \
git \
memcached \
libmemcached-tools
# Debian's memcached also listens on ::1 and does not start when IPv6 is disabled
sed -i '/^-l ::1$/d' /etc/memcached.conf
systemctl restart memcached
msg_ok "Installed Dependencies"
PYTHON_VERSION="3.13" setup_uv
+8 -2
View File
@@ -48,6 +48,7 @@ chown -R archivebox:archivebox /opt/archivebox/{data,.npm,.cache,.local}
chmod -R 755 /opt/archivebox/data
$STD uv pip install archivebox --system --break-system-packages
cd /opt/archivebox/data
ADMIN_PASS=$(random_password 20)
expect <<EOF
set timeout -1
log_user 0
@@ -62,13 +63,18 @@ expect "Email address"
send "\r"
expect "Password"
send "community-scripts.org\r"
send "${ADMIN_PASS}\r"
expect "Password (again)"
send "community-scripts.org\r"
send "${ADMIN_PASS}\r"
expect eof
EOF
{
echo "ArchiveBox Credentials"
echo "Username: archivebox"
echo "Password: ${ADMIN_PASS}"
} >~/archivebox.creds
msg_ok "Installed ArchiveBox"
msg_info "Creating Service"
+8 -1
View File
@@ -63,12 +63,19 @@ else
fi
echo "$AUTHELIA_URL" >/etc/authelia/.authelia_url
AUTHELIA_PASS=$(random_password 20)
AUTHELIA_HASH=$(authelia crypto hash generate argon2 --password "$AUTHELIA_PASS" | awk '/Digest:/{print $2}')
{
echo "Authelia Credentials"
echo "Username: authelia"
echo "Password: ${AUTHELIA_PASS}"
} >~/authelia.creds
cat <<EOF >/etc/authelia/users.yml
users:
authelia:
disabled: false
displayname: "Authelia Admin"
password: "\$argon2id\$v=19\$m=65536,t=3,p=4\$ZBopMzXrzhHXPEZxRDVT2w\$SxWm96DwhOsZyn34DLocwQEIb4kCDsk632PuiMdZnig"
password: "${AUTHELIA_HASH}"
groups: []
EOF
cat <<EOF >/etc/authelia/configuration.yml
+16 -7
View File
@@ -23,10 +23,15 @@ setup_deb_based() {
setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "binary"
msg_info "Setup xCaddy"
$STD apt install -y git
$STD xcaddy build
msg_ok "Setup xCaddy"
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy is installed; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
fi
}
@@ -69,10 +74,14 @@ EOF
GO_VERSION="$(curl -fsSL https://go.dev/VERSION?m=text | head -1 | cut -c3-)" setup_go
fetch_and_deploy_gh_release "xcaddy" "caddyserver/xcaddy" "prebuild" "latest" "/opt/xcaddy" "xcaddy_*_linux_$(arch_resolve).tar.gz"
ln -sf /opt/xcaddy/xcaddy /usr/local/bin/xcaddy
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
if (($(awk '/^MemTotal:/ {print int($2 / 1024)}' /proc/meminfo) < 1900)); then
msg_warn "xCaddy is installed; its test build was skipped because linking Caddy needs about 2 GB RAM"
else
msg_info "Setup xCaddy"
cd /opt
$STD xcaddy build
msg_ok "Setup xCaddy"
fi
fi
msg_info "Enabling Caddy Service"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/chattocorp/chatto
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "chatto" "chattocorp/chatto" "prebuild" "latest" "/opt/chatto" "chatto_Linux_$(arch_resolve x86_64 arm64).tar.gz"
chmod +x /opt/chatto/chatto
msg_info "Configuring Chatto"
mkdir -p /opt/chatto_data
cd /opt/chatto
$STD ./chatto init
mv /opt/chatto/chatto.toml /opt/chatto_data/chatto.toml
sed -i \
-e "/^\[webserver\]/,/^\[/ s|^url = .*|url = 'http://${LOCAL_IP}:4000'|" \
-e "/^\[nats.embedded\]/,/^\[/ s|^data_dir = .*|data_dir = '/opt/chatto_data/data'|" \
-e '/^# \[operator_api\]$/,/^$/ { s|^# \[operator_api\]$|[operator_api]|; s|^# enabled = false$|enabled = true| }' \
/opt/chatto_data/chatto.toml
msg_ok "Configured Chatto"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/chatto.service
[Unit]
Description=Chatto
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/chatto
ExecStart=/opt/chatto/chatto run -c /opt/chatto_data/chatto.toml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now chatto
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+2 -2
View File
@@ -24,7 +24,7 @@ $STD apt install -y \
msg_ok "Installed Dependencies"
setup_ffmpeg
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
fetch_and_deploy_gh_release "cinephage" "MoldyTaint/Cinephage" "tarball"
msg_info "Building Cinephage"
@@ -35,7 +35,7 @@ $STD npm prune --omit=dev
msg_ok "Built Cinephage"
msg_info "Installing Camoufox"
$STD /opt/cinephage/node_modules/.bin/camoufox-js fetch
$STD /opt/cinephage/node_modules/.bin/camoufox fetch
msg_ok "Installed Camoufox"
msg_info "Configuring Cinephage"
+1 -1
View File
@@ -17,7 +17,7 @@ msg_info "Installing Dependencies"
$STD apt install -y build-essential
msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
msg_info "Setup Cross-Seed"
$STD npm install cross-seed@latest -g
+14 -86
View File
@@ -16,119 +16,47 @@ update_os
msg_info "Installing Dependencies"
$STD apt install -y \
nginx \
valkey \
mariadb-client \
rclone
libedit2 \
libgssapi-krb5-2 \
libncurses6 \
libreadline8t64
msg_ok "Installed Dependencies"
PG_VERSION="17" setup_postgresql
NODE_VERSION="24" NODE_MODULE="corepack" setup_nodejs
msg_info "Installing Database Clients"
# Create PostgreSQL version symlinks for compatibility
for v in 12 13 14 15 16 18; do
ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v
done
# Install MongoDB Database Tools via direct .deb (no APT repo for Debian 13)
[[ "$(get_os_info id)" == "ubuntu" ]] && MONGO_DIST="ubuntu2204" || MONGO_DIST="debian12"
# MongoDB only publishes arm64 builds for Ubuntu
[[ "$(arch_resolve "x86_64" "arm64")" == "arm64" ]] && MONGO_DIST="ubuntu2204"
MONGO_VERSION=$(get_latest_gh_tag "mongodb/mongo-tools" "100." || echo "100.16.1")
fetch_and_deploy_from_url "https://fastdl.mongodb.org/tools/db/mongodb-database-tools-${MONGO_DIST}-$(arch_resolve "x86_64" "arm64")-${MONGO_VERSION}.deb" ""
mkdir -p /usr/local/mongodb-database-tools/bin
[[ -f /usr/bin/mongodump ]] && ln -sf /usr/bin/mongodump /usr/local/mongodb-database-tools/bin/mongodump
[[ -f /usr/bin/mongorestore ]] && ln -sf /usr/bin/mongorestore /usr/local/mongodb-database-tools/bin/mongorestore
# Create MariaDB and MySQL client symlinks for compatibility
mkdir -p /usr/local/mariadb-{10.6,12.1}/bin /usr/local/mysql-{5.7,8.0,8.4,9}/bin
for dir in /usr/local/mariadb-{10.6,12.1}/bin; do
ln -sf /usr/bin/mariadb-dump "$dir/mariadb-dump"
ln -sf /usr/bin/mariadb "$dir/mariadb"
done
for dir in /usr/local/mysql-{5.7,8.0,8.4,9}/bin; do
ln -sf /usr/bin/mariadb-dump "$dir/mysqldump"
ln -sf /usr/bin/mariadb "$dir/mysql"
done
msg_ok "Installed Database Clients"
fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus"
GO_VERSION="$(grep -m1 '^go ' /opt/databasus/backend/go.mod | awk '{print $2}')" setup_go
msg_info "Building Databasus (Patience)"
RELEASE=$(cat ~/.databasus)
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
cd /opt/databasus/frontend
$STD corepack prepare pnpm@latest --activate
$STD pnpm install --frozen-lockfile
$STD pnpm run build
$STD env VITE_APP_VERSION="${RELEASE}" pnpm run build
cd /opt/databasus/backend
$STD go mod tidy
$STD go mod download
$STD go install github.com/swaggo/swag/cmd/swag@latest
$STD /root/go/bin/swag init -g cmd/main.go -o swagger
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$(arch_resolve) go build -o databasus ./cmd
mv /opt/databasus/backend/databasus /opt/databasus/databasus
$STD env CGO_ENABLED=0 GOOS=linux GOARCH="$(arch_resolve)" go build -o /opt/databasus/databasus ./cmd/databasus
mkdir -p /opt/databasus/agent-binaries
for arch in amd64 arm64; do
$STD env CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -ldflags "-X main.Version=${RELEASE}" -o /opt/databasus/agent-binaries/databasus-verification-agent-linux-$arch ./cmd/verification-agent
done
mkdir -p /databasus-data/{pgdata,temp,backups,data,logs}
mkdir -p /opt/databasus/ui/build
mkdir -p /opt/databasus/migrations
cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/
cp -r /opt/databasus/backend/migrations/* /opt/databasus/migrations/
chown -R postgres:postgres /databasus-data
msg_ok "Built Databasus"
msg_info "Configuring Databasus"
JWT_SECRET=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
# Install goose for migrations
$STD go install github.com/pressly/goose/v3/cmd/goose@latest
ln -sf /root/go/bin/goose /usr/local/bin/goose
cat <<EOF >/.env
# Environment
ENV_MODE=production
# Server
SERVER_PORT=4005
SERVER_HOST=0.0.0.0
# Database
APP_VERSION=${RELEASE}
DATABASE_DSN=host=localhost user=postgres password=postgres dbname=databasus port=5432 sslmode=disable
DATABASE_URL=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable
# Migrations
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable
GOOSE_MIGRATION_DIR=/opt/databasus/migrations
# Valkey (Redis-compatible cache)
VALKEY_HOST=localhost
VALKEY_PORT=6379
# Security
JWT_SECRET=${JWT_SECRET}
ENCRYPTION_KEY=${ENCRYPTION_KEY}
# Paths
DATA_DIR=/databasus-data/data
BACKUP_DIR=/databasus-data/backups
LOG_DIR=/databasus-data/logs
EOF
chmod 600 /.env
msg_ok "Configured Databasus"
msg_info "Configuring Valkey"
cat <<EOF >/etc/valkey/valkey.conf
port 6379
bind 127.0.0.1
protected-mode yes
save ""
maxmemory 256mb
maxmemory-policy allkeys-lru
EOF
systemctl enable -q --now valkey-server
systemctl restart valkey-server
msg_ok "Configured Valkey"
msg_info "Creating Database"
# Configure PostgreSQL to allow local password auth for databasus
PG_HBA="/etc/postgresql/17/main/pg_hba.conf"
if ! grep -q "databasus" "$PG_HBA"; then
sed -i '/^local\s*all\s*all/i local databasus postgres trust' "$PG_HBA"
@@ -143,8 +71,8 @@ msg_info "Creating Databasus Service"
cat <<EOF >/etc/systemd/system/databasus.service
[Unit]
Description=Databasus - Database Backup Management
After=network.target postgresql.service valkey.service
Requires=postgresql.service valkey.service
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
+28 -1
View File
@@ -25,9 +25,36 @@ $STD apt install -y \
gsfonts \
brotli \
nginx \
redis-server
redis-server \
meson \
ninja-build \
pkg-config \
libglib2.0-dev \
libexpat1-dev \
libjpeg-dev \
libpng-dev \
libwebp-dev \
libtiff-dev \
libheif-dev \
libjxl-dev \
librsvg2-dev \
libcgif-dev \
libexif-dev \
liblcms2-dev \
liborc-0.4-dev \
libimagequant-dev \
libpango1.0-dev
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "libvips" "libvips/libvips" "prebuild" "latest" "/opt/libvips" "vips-*.tar.xz"
msg_info "Building libvips"
cd /opt/libvips
$STD meson setup build --prefix=/usr/local --libdir=lib --buildtype=release -Dmodules=disabled -Dmagick=disabled -Dexamples=false
$STD ninja -C build install
ldconfig
msg_ok "Built libvips"
msg_info "Cloning Discourse"
$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse
msg_ok "Cloned Discourse"
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: Masked-Kunsiquat
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://www.espocrm.com/ | Github: https://github.com/espocrm/espocrm
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
if [[ -z "${var_admin_user:-}" ]]; then
var_admin_user=$(prompt_input "EspoCRM admin username (a-z, 0-9, @ . _ -):" "admin" 60)
fi
var_admin_user="${var_admin_user:-admin}"
var_admin_user="${var_admin_user,,}"
if [[ ! "$var_admin_user" =~ ^[a-z0-9@._-]+$ ]]; then
msg_warn "EspoCRM usernames take only a-z, 0-9 and @ . _ -; using admin"
var_admin_user="admin"
fi
if [[ -z "${var_admin_pass:-}" ]]; then
var_admin_pass=$(random_password 16)
fi
msg_info "Installing Dependencies"
$STD apt install -y nginx
msg_ok "Installed Dependencies"
setup_mariadb
MARIADB_DB_NAME="espocrm" MARIADB_DB_USER="espocrm" setup_mariadb_db
PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="ldap" setup_php
fetch_and_deploy_gh_release "espo-crm" "espocrm/espocrm" "prebuild" "latest" "/opt/espocrm" "EspoCRM-*.zip"
msg_info "Configuring EspoCRM"
cd /opt/espocrm
$STD php bin/command config:populate
$STD php bin/command config:set defaultPermissions.user www-data
$STD php bin/command config:set defaultPermissions.group www-data
$STD php bin/command config:set database.platform Mysql
$STD php bin/command config:set database.host localhost
$STD php bin/command config:set database.dbname "$MARIADB_DB_NAME"
$STD php bin/command config:set database.user "$MARIADB_DB_USER"
$STD php bin/command config:set database.password "$MARIADB_DB_PASS"
$STD php bin/command rebuild
$STD php bin/command create-admin-user "$var_admin_user"
printf '%s\n' "$var_admin_pass" | $STD php bin/command set-password "$var_admin_user"
$STD php bin/command config:set siteUrl "http://${LOCAL_IP}"
$STD php bin/command populate-scheduled-jobs
$STD php bin/command config:set jobRunInParallel true --type=bool
$STD php bin/command app-check
$STD php bin/command config:set isInstalled true --type=bool
chown -R www-data:www-data /opt/espocrm
msg_ok "Configured EspoCRM"
msg_info "Creating Nginx Site"
PHP_SOCK=$(get_php_fpm_socket)
cat <<EOF >/etc/nginx/sites-available/espocrm
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
charset utf-8;
index index.html index.php;
client_max_body_size 128M;
keepalive_timeout 300;
server_tokens off;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
gzip on;
gzip_types text/plain text/css text/javascript application/javascript application/json;
gzip_min_length 1000;
root /opt/espocrm/public;
location /client {
root /opt/espocrm;
autoindex off;
location ~* ^.+\.(js|css|png|jpg|svg|svgz|jpeg|gif|ico|tpl)\$ {
access_log off;
expires max;
}
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location ~ \.php\$ {
fastcgi_pass unix:${PHP_SOCK};
include fastcgi_params;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
fastcgi_param QUERY_STRING \$query_string;
}
location /api/v1/ {
if (!-e \$request_filename) {
rewrite ^/api/v1/(.*)\$ /api/v1/index.php last;
}
}
location /portal/ {
try_files \$uri \$uri/ /portal/index.php?\$query_string;
}
location /api/v1/portal-access {
if (!-e \$request_filename) {
rewrite ^/api/v1/(.*)\$ /api/v1/portal-access/index.php last;
}
}
location ~ /(\.htaccess|web\.config|\.git) {
deny all;
}
}
EOF
msg_ok "Created Nginx Site"
nginx_enable_site "espocrm"
msg_info "Setting up Cron"
cat <<EOF >/etc/cron.d/espocrm
* * * * * www-data cd /opt/espocrm && php -f cron.php >/dev/null 2>&1
EOF
systemctl enable -q --now cron
msg_ok "Set up Cron"
msg_info "Saving Credentials"
cat <<EOF >>~/espocrm.creds
EspoCRM Admin
Username: ${var_admin_user}
Password: ${var_admin_pass}
EOF
chmod 600 ~/espocrm.creds
msg_ok "Saved Credentials"
motd_ssh
customize
cleanup_lxc
+1
View File
@@ -26,6 +26,7 @@ chown -R www-data:www-data /opt/firefly
chmod -R 775 /opt/firefly/storage
cd /opt/firefly
cp .env.example .env
sed -i -e "s|^APP_KEY=.*|APP_KEY=$(random_password 32)|" -e "s|^STATIC_CRON_TOKEN=.*|STATIC_CRON_TOKEN=$(random_password 32)|" /opt/firefly/.env
sed -i "s/DB_HOST=.*/DB_HOST=localhost/" /opt/firefly/.env
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$MARIADB_DB_PASS/" /opt/firefly/.env
$STD composer install --no-dev --no-plugins --no-interaction
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/mozilla-services/syncstorage-rs
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
pkg-config \
libssl-dev \
libpq-dev \
python3-dev
msg_ok "Installed Dependencies"
PG_VERSION="18" setup_postgresql
PG_DB_NAME="syncserver" PG_DB_USER="syncserver" setup_postgresql_db
setup_rust
setup_uv
fetch_and_deploy_gh_release "firefox-syncserver" "mozilla-services/syncstorage-rs" "tarball"
msg_info "Building Firefox Syncserver (Patience)"
cd /opt/firefox-syncserver
$STD cargo build --release --locked --no-default-features --features=syncstorage-db/postgres --features=tokenserver-db/postgres --features=py_verifier
cp target/release/syncserver /usr/local/bin/syncserver
rm -rf target
$STD uv venv --python /usr/bin/python3 /opt/firefox-syncserver/.venv
$STD uv pip install --python /opt/firefox-syncserver/.venv/bin/python pyfxa tokenlib cryptography
msg_ok "Built Firefox Syncserver"
msg_info "Configuring Firefox Syncserver"
mkdir -p /opt/firefox-syncserver_data
cat <<EOF >/opt/firefox-syncserver_data/.env
SYNC_HOST=0.0.0.0
SYNC_PORT=8000
SYNC_MASTER_SECRET=$(openssl rand -hex 32)
SYNC_HUMAN_LOGS=true
RUST_LOG=info
SYNC_SYNCSTORAGE__DATABASE_URL=postgres://syncserver:${PG_DB_PASS}@127.0.0.1:5432/syncserver
SYNC_SYNCSTORAGE__GLEAN_ENABLED=false
SYNC_TOKENSERVER__ENABLED=true
SYNC_TOKENSERVER__RUN_MIGRATIONS=true
SYNC_TOKENSERVER__DATABASE_URL=postgres://syncserver:${PG_DB_PASS}@127.0.0.1:5432/syncserver
SYNC_TOKENSERVER__NODE_TYPE=postgres
SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN=api.accounts.firefox.com
SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL=https://oauth.accounts.firefox.com
SYNC_TOKENSERVER__INIT_NODE_URL=http://${LOCAL_IP}:8000
EOF
chmod 600 /opt/firefox-syncserver_data/.env
cat <<'EOF' >/opt/firefox-syncserver_data/nodes.sql
SELECT to_regclass('nodes') IS NOT NULL AS ready \gset
\if :ready
UPDATE nodes SET node = :'url'
WHERE id = (SELECT min(id) FROM nodes)
AND NOT EXISTS (SELECT 1 FROM nodes WHERE node = :'url');
UPDATE users SET nodeid = (SELECT id FROM nodes WHERE node = :'url')
WHERE nodeid <> (SELECT id FROM nodes WHERE node = :'url');
DELETE FROM nodes WHERE node <> :'url';
\endif
EOF
msg_ok "Configured Firefox Syncserver"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/firefox-syncserver.service
[Unit]
Description=Firefox Syncserver
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/firefox-syncserver
EnvironmentFile=/opt/firefox-syncserver_data/.env
Environment=PYTHONPATH=/opt/firefox-syncserver/.venv/lib/python3.13/site-packages
ExecStartPre=/usr/bin/psql -1 -q -v ON_ERROR_STOP=1 -v url=\${SYNC_TOKENSERVER__INIT_NODE_URL} -f /opt/firefox-syncserver_data/nodes.sql \${SYNC_TOKENSERVER__DATABASE_URL}
ExecStart=/usr/local/bin/syncserver
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now firefox-syncserver
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc
+21 -26
View File
@@ -14,18 +14,15 @@ network_check
update_os
msg_info "Installing Dependencies"
CODENAME=$(get_os_info codename)
setup_deb822_repo \
"debian-backports" \
"file:///usr/share/keyrings/debian-archive-keyring.gpg" \
"http://deb.debian.org/debian" \
"${CODENAME}-backports" \
"main"
$STD apt install -y \
chromium \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
libreoffice-draw \
libreoffice-math \
python3-uno \
python-is-python3 \
pdftk-java \
qpdf \
libimage-exiftool-perl \
fonts-crosextra-carlito \
fonts-crosextra-caladea \
fonts-liberation \
@@ -33,28 +30,30 @@ $STD apt install -y \
fonts-dejavu \
fonts-noto-cjk \
fonts-noto-color-emoji \
fonts-noto-core
fonts-noto-core \
fonts-urw-base35
# lok is built against the LibreOffice from backports
$STD apt install -y -t "${CODENAME}-backports" \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
libreoffice-draw \
libreoffice-math
msg_ok "Installed Dependencies"
fetch_and_deploy_gh_release "pdfcpu" "pdfcpu/pdfcpu" "prebuild" "latest" "/opt/pdfcpu" "pdfcpu_*_Linux_$(arch_resolve "x86_64" "arm64").tar.xz"
ln -sf "$(find /opt/pdfcpu -type f -name pdfcpu | head -n1)" /usr/local/bin/pdfcpu
msg_info "Installing unoconverter"
UNOCONVERTER_VERSION=$(get_latest_github_release "gotenberg/unoconverter" "false")
download_file "https://raw.githubusercontent.com/gotenberg/unoconverter/${UNOCONVERTER_VERSION}/unoconv" /usr/local/bin/unoconverter
chmod +x /usr/local/bin/unoconverter
msg_ok "Installed unoconverter"
fetch_and_deploy_gh_release "gotenberg" "gotenberg/gotenberg" "tarball" "latest" "/opt/gotenberg"
GO_VERSION="$(awk '$1=="go"{print $2}' /opt/gotenberg/go.mod | cut -d. -f1,2)" setup_go
LOK_VERSION="$(awk -F= '/^ARG LOK_VERSION=/{print $2}' /opt/gotenberg/build/Dockerfile)"
fetch_and_deploy_gh_release "lok" "gotenberg/lok" "prebuild" "$LOK_VERSION" "/usr/local/bin" "lok_${LOK_VERSION#v}_linux_$(arch_resolve "amd64" "arm64").tar.gz"
msg_info "Building Gotenberg (Patience)"
cd /opt/gotenberg
export CGO_ENABLED=0
$STD go mod download
$STD go build -o /usr/local/bin/gotenberg \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$(cat ~/.gotenberg)'" \
-ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v9/cmd.Version=$(cat ~/.gotenberg)'" \
cmd/gotenberg/main.go
msg_ok "Built Gotenberg"
@@ -70,14 +69,10 @@ Type=simple
User=root
Environment=LANG=C.UTF-8
Environment=LC_ALL=C.UTF-8
Environment=PDFTK_BIN_PATH=/usr/bin/pdftk
Environment=QPDF_BIN_PATH=/usr/bin/qpdf
Environment=EXIFTOOL_BIN_PATH=/usr/bin/exiftool
Environment=PDFCPU_BIN_PATH=/usr/local/bin/pdfcpu
Environment=CHROMIUM_BIN_PATH=/usr/bin/chromium
Environment=CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/build/chromium-hyphen-data
Environment=LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
Environment=UNOCONVERTER_BIN_PATH=/usr/local/bin/unoconverter
Environment=LOK_BIN_PATH=/usr/local/bin/lok
Environment=OTEL_TRACES_EXPORTER=none
Environment=OTEL_METRICS_EXPORTER=none
Environment=OTEL_LOGS_EXPORTER=none
+7 -1
View File
@@ -33,7 +33,13 @@ msg_ok "Set up Python Backend"
msg_info "Configuring Homelable"
mkdir -p /opt/homelable/data
SECRET_KEY=$(openssl rand -hex 32)
BCRYPT_HASH=$(/opt/homelable/backend/.venv/bin/python -c "import bcrypt; print(bcrypt.hashpw(b'admin', bcrypt.gensalt()).decode())")
HOMELABLE_PASS=$(random_password 20)
BCRYPT_HASH=$(/opt/homelable/backend/.venv/bin/python -c "import bcrypt,sys; print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt()).decode())" "$HOMELABLE_PASS")
{
echo "Homelable Credentials"
echo "Username: admin"
echo "Password: ${HOMELABLE_PASS}"
} >~/homelable.creds
cat <<EOF >/opt/homelable/backend/.env
SECRET_KEY=${SECRET_KEY}
SQLITE_PATH=/opt/homelable/data/homelab.db
+56 -28
View File
@@ -18,7 +18,12 @@ if is_debian && [[ "$(get_os_version_major)" -lt 13 ]]; then
exit 238
fi
RELEASE="v3.3.5"
if [[ "$(arch_resolve)" == "amd64" ]] && ! grep -qm1 sse4_2 /proc/cpuinfo; then
msg_error "Immich needs a CPU with SSE4.2: its web build stops with 'Illegal instruction' on older ones."
exit 236
fi
RELEASE="v3.4.1"
if lscpu | grep -q 'GenuineIntel'; then
echo ""
echo ""
@@ -63,6 +68,7 @@ msg_info "Installing dependencies"
$STD apt install --no-install-recommends -y \
git \
redis \
pigz \
autoconf \
build-essential \
python3-dev \
@@ -108,6 +114,7 @@ $STD apt install --no-install-recommends -y \
libhwy-dev \
libwebp-dev \
libaom-dev \
unzip \
ccache
setup_deb822_repo \
@@ -129,6 +136,28 @@ if [[ "$CTTYPE" == "0" && -d /dev/dri ]]; then
fi
msg_ok "Dependencies Installed"
INSTALL_DIR="/opt/${APPLICATION}"
UPLOAD_DIR="${INSTALL_DIR}/upload"
SRC_DIR="${INSTALL_DIR}/source"
APP_DIR="${INSTALL_DIR}/app"
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
msg_info "Installing GeoNames data"
cd "$GEO_DIR"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
CURL_RETRIES=6 CURL_CONNECT_TO=30 curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
rm cities500.zip
cd ~
msg_ok "Installed GeoNames data"
MISE_ARCH="x64"
[[ "$(arch_resolve)" == "arm64" ]] && MISE_ARCH="arm64"
fetch_and_deploy_gh_release "mise" "jdx/mise" "singlefile" "latest" "/usr/local/bin" "mise-v*-linux-${MISE_ARCH}"
@@ -167,7 +196,7 @@ ACTUAL_PG_VERSION=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -1)
ACTUAL_PG_VERSION=${ACTUAL_PG_VERSION:-16}
VCHORD_RELEASE="1.1.1"
fetch_and_deploy_gh_release "VectorChord" "tensorchord/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${ACTUAL_PG_VERSION}-vchord_*_$(arch_resolve).deb"
fetch_and_deploy_gh_release "VectorChord" "supervc-stack/VectorChord" "binary" "${VCHORD_RELEASE}" "/tmp" "postgresql-${ACTUAL_PG_VERSION}-vchord_*_$(arch_resolve).deb"
sed -i "s/^#shared_preload.*/shared_preload_libraries = 'vchord.so'/" /etc/postgresql/${ACTUAL_PG_VERSION}/main/postgresql.conf
systemctl restart postgresql.service
@@ -194,7 +223,7 @@ LIBJXL_REVISION="$(jq -cr '.revision' "$BASE_DIR"/server/sources/libjxl.json)"
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
cd "$SOURCE"
$STD git reset --hard "$LIBJXL_REVISION"
$STD git submodule update --init --recursive --depth 1 --recommend-shallow
$STD git submodule update --init --recursive --depth 1 --recommend-shallow -- third_party
mkdir build
cd build
$STD cmake \
@@ -347,15 +376,6 @@ libvips: $LIBVIPS_REVISION
EOF
msg_ok "Custom Photo-processing Libraries Compiled Successfully"
INSTALL_DIR="/opt/${APPLICATION}"
UPLOAD_DIR="${INSTALL_DIR}/upload"
SRC_DIR="${INSTALL_DIR}/source"
APP_DIR="${INSTALL_DIR}/app"
PLUGIN_DIR="${APP_DIR}/plugins/immich-plugin-core"
ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "${RELEASE}" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
export COREPACK_ENABLE_DOWNLOAD_PROMPT=0
@@ -396,7 +416,7 @@ cp -a web/build "$APP_DIR"/www
cp LICENSE "$APP_DIR"
cd "$SRC_DIR"
export MISE_TRUSTED_CONFIG_PATHS="$SRC_DIR"/mise.toml
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg
export MISE_DISABLE_TOOLS=github:jellyfin/jellyfin-ffmpeg,java,terragrunt,opentofu,restic,npm:@openapitools/openapi-generator-cli
export MISE_LOCKFILE_MODE=merge MISE_LOCKFILE_PLATFORMS="linux-$(arch_resolve x64 arm64)"
mise_ok=0
for i in 1 2 3; do
@@ -414,7 +434,8 @@ if ! command -v extism-js >/dev/null 2>&1; then
# fetch_and_deploy_gh_release cannot deploy (singlefile leaves it compressed,
# prebuild only handles zip/tar). Fetch + gunzip it directly.
EXTISM_ARCH="$(arch_resolve x86_64 aarch64)"
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-${EXTISM_ARCH}-linux-v1.6.0.gz"
EXTISM_VERSION="$(grep -oP '"github:extism/js-pdk"\s*=\s*"\K[^"]+' "$SRC_DIR"/mise.toml)"
curl_download /tmp/extism-js.gz "https://github.com/extism/js-pdk/releases/download/${EXTISM_VERSION}/extism-js-${EXTISM_ARCH}-linux-${EXTISM_VERSION}.gz"
gunzip -f /tmp/extism-js.gz
install -m 0755 /tmp/extism-js /usr/local/bin/extism-js
rm -f /tmp/extism-js
@@ -426,6 +447,25 @@ cp -r ./packages/plugin-core/dist "$PLUGIN_DIR"/dist
cp ./packages/plugin-core/manifest.json "$PLUGIN_DIR"
msg_ok "Installed Immich Server, Web and Plugin Components"
RESTIC_REF="$(grep -oP 'futo-org/restic:sha-\K[0-9a-f]+' "$SRC_DIR"/server/Dockerfile)"
RESTIC_PROXY_REF="$(grep -oP 'futo-org/backups/restic-proxy:\K[^@]+' "$SRC_DIR"/server/Dockerfile)"
FUTO_DIR=/opt/staging/futo
rm -rf "$FUTO_DIR"
$STD git clone --filter=blob:none --no-checkout https://github.com/futo-org/restic "$FUTO_DIR"/restic
$STD git -C "$FUTO_DIR"/restic checkout "$RESTIC_REF"
$STD git clone --depth 1 --filter=blob:none --sparse -b "$RESTIC_PROXY_REF" https://github.com/futo-org/backups "$FUTO_DIR"/backups
$STD git -C "$FUTO_DIR"/backups sparse-checkout set packages/restic-proxy
GO_VERSION="$(awk '/^go /{print $2}' "$FUTO_DIR"/backups/packages/restic-proxy/go.mod)" setup_go
msg_info "Building restic for FUTO Backups"
cd "$FUTO_DIR"/restic
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic ./cmd/restic
cd "$FUTO_DIR"/backups/packages/restic-proxy
$STD env CGO_ENABLED=0 go build -o /usr/local/bin/restic-proxy .
cd "$SRC_DIR"
rm -rf "$FUTO_DIR"
echo "${RESTIC_REF} ${RESTIC_PROXY_REF}" >~/.immich_restic
msg_ok "Built restic for FUTO Backups"
cd "$SRC_DIR"/machine-learning
$STD useradd -U -s /usr/sbin/nologin -r -M -d "$INSTALL_DIR" immich
mkdir -p "$ML_DIR"
@@ -444,7 +484,7 @@ if [[ -f ~/.openvino ]]; then
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing Intel OpenVINO machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra openvino --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -462,7 +502,7 @@ else
msg_ok "Pre-installed Python ${ML_PYTHON}"
msg_info "Installing machine-learning"
for attempt in $(seq 1 3); do
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
$STD sudo --preserve-env=VIRTUAL_ENV,UV_HTTP_TIMEOUT,UV_CONCURRENT_DOWNLOADS,UV_CONCURRENT_BUILDS,UV_CONCURRENT_INSTALLS -Pnu immich uv sync --extra cpu --no-dev --upgrade-package watchfiles --active --link-mode copy -n -p "${ML_PYTHON}" --managed-python && break
[[ $attempt -eq 3 ]] && { msg_error "uv sync failed three times, the machine-learning environment was not built"; exit 1; }
# Parallel fetches can trip DNS rate limits (AdGuard, Pi-hole); retry one at a time.
export UV_CONCURRENT_DOWNLOADS=1 UV_CONCURRENT_BUILDS=1 UV_CONCURRENT_INSTALLS=1
@@ -481,20 +521,8 @@ grep -rlE "'/build'" | xargs -n1 sed -i "s|'/build'|'$APP_DIR'|g"
sed -i "s@\"/cache\"@\"$INSTALL_DIR/cache\"@g" "$ML_DIR"/immich_ml/config.py
ln -s "$UPLOAD_DIR" "$APP_DIR"/upload
ln -s "$UPLOAD_DIR" "$ML_DIR"/upload
msg_info "Installing GeoNames data"
cd "$GEO_DIR"
curl_with_retry "https://download.geonames.org/export/dump/admin1CodesASCII.txt" "admin1CodesASCII.txt"
curl_with_retry "https://download.geonames.org/export/dump/admin2Codes.txt" "admin2Codes.txt"
curl_with_retry "https://download.geonames.org/export/dump/countryInfo.txt" "countryInfo.txt"
curl_with_retry "https://download.geonames.org/export/dump/cities500.zip" "cities500.zip"
curl_with_retry "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/v5.1.2/geojson/ne_10m_admin_0_countries.geojson" "ne_10m_admin_0_countries.geojson"
unzip -q cities500.zip
date --iso-8601=seconds | tr -d "\n" >geodata-date.txt
rm cities500.zip
cd "$INSTALL_DIR"
ln -s "$GEO_DIR" "$APP_DIR"
msg_ok "Installed GeoNames data"
# MickLesk temporary patch for HEIC thumbnail gen
MEDIA_REPO_JS="/opt/immich/app/dist/repositories/media.repository.js"
+1 -1
View File
@@ -21,7 +21,7 @@ msg_ok "Installed Dependencies"
PG_VERSION="16" setup_postgresql
PG_DB_NAME="kan" PG_DB_USER="kan" setup_postgresql_db
NODE_VERSION="20" NODE_MODULE="pnpm" setup_nodejs
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
fetch_and_deploy_gh_tag "kan" "kanbn/kan" "latest"
+9 -1
View File
@@ -21,6 +21,7 @@ PG_DB_NAME="keycloak" PG_DB_USER="keycloak" setup_postgresql_db
fetch_and_deploy_gh_release "keycloak_app" "keycloak/keycloak" "prebuild" "latest" "/opt/keycloak" "keycloak-*.tar.gz"
msg_info "Creating Service"
ADMIN_PASS=$(random_password 20)
cat <<EOF >/etc/systemd/system/keycloak.service
[Unit]
Description=Keycloak Service
@@ -40,7 +41,7 @@ Environment="KC_DB_USERNAME=keycloak"
Environment="KC_DB_PASSWORD=$PG_DB_PASS"
Environment="KC_HTTP_ENABLED=true"
Environment="KC_BOOTSTRAP_ADMIN_USERNAME=tmpadm"
Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=admin123"
Environment="KC_BOOTSTRAP_ADMIN_PASSWORD=${ADMIN_PASS}"
# Comment following line and uncomment the next 2 if working behind a reverse proxy
Environment="KC_HOSTNAME_STRICT=false"
#Environment="KC_HOSTNAME=keycloak.example.com"
@@ -48,6 +49,13 @@ Environment="KC_HOSTNAME_STRICT=false"
[Install]
WantedBy=multi-user.target
EOF
chmod 600 /etc/systemd/system/keycloak.service
{
echo "Keycloak Credentials"
echo "Temporary admin, replace it with a permanent one"
echo "Username: tmpadm"
echo "Password: ${ADMIN_PASS}"
} >~/keycloak.creds
systemctl enable -q --now keycloak
msg_ok "Created Service"
+8 -1
View File
@@ -40,6 +40,7 @@ cp .env.dist .env
sed -i "/^DATABASE_URL=.*/c\DATABASE_URL=mysql://kimai:$MARIADB_DB_PASS@127.0.0.1:3306/kimai_db?charset=utf8mb4&serverVersion=mariadb-$MYSQL_VERSION" /opt/kimai/.env
sed -i "s|^APP_SECRET=.*|APP_SECRET=$APP_SECRET|" /opt/kimai/.env
$STD bin/console kimai:install -n
ADMIN_PASS=$(random_password 20)
$STD expect <<EOF
set timeout -1
log_user 0
@@ -47,10 +48,16 @@ log_user 0
spawn bin/console kimai:user:create admin admin@community-scripts.org ROLE_SUPER_ADMIN
expect "Please enter the password:"
send "community-scripts.org\r"
send "${ADMIN_PASS}\r"
expect eof
EOF
{
echo "Kimai Credentials"
echo "Username: admin"
echo "Email: admin@community-scripts.org"
echo "Password: ${ADMIN_PASS}"
} >~/kimai.creds
$STD composer update --no-interaction
cat <<EOF >/opt/kimai/config/packages/local.yaml
kimai:
+1 -1
View File
@@ -26,7 +26,7 @@ NODE_VERSION="24" NODE_MODULE="corepack,pnpm" setup_nodejs
msg_info "Installing Deno"
export DENO_INSTALL="/usr/local"
curl -fsSL https://deno.land/install.sh | $STD sh -s -- -y
curl -fsSL https://deno.land/install.sh | CI=1 $STD sh -s
[[ ":$PATH:" != *":/usr/local/bin:"* ]] &&
echo -e "\nexport PATH=\"/usr/local/bin:\$PATH\"" >>~/.bashrc &&
source ~/.bashrc
+7 -1
View File
@@ -33,7 +33,13 @@ $STD yarn config set ignore-engines true
$STD yarn install
$STD yarn run production
$STD php artisan key:generate
$STD php artisan setup:production --email=admin@community-scripts.org --password=community-scripts.org --force
ADMIN_PASS=$(random_password 20)
$STD php artisan setup:production --email=admin@community-scripts.org --password="${ADMIN_PASS}" --force
{
echo "Monica Credentials"
echo "Email: admin@community-scripts.org"
echo "Password: ${ADMIN_PASS}"
} >~/monica.creds
chown -R www-data:www-data /opt/monica
chmod -R 775 /opt/monica/storage
echo "* * * * * root php /opt/monica/artisan schedule:run >> /dev/null 2>&1" >>/etc/crontab
+1 -1
View File
@@ -13,7 +13,7 @@ setting_up_container
network_check
update_os
NODE_VERSION="24" setup_nodejs
NODE_VERSION="26" setup_nodejs
fetch_and_deploy_gh_release "myip" "jason5ng32/MyIP" "tarball"
msg_info "Configuring MyIP"
+4 -1
View File
@@ -23,7 +23,10 @@ msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
msg_info "Installing n8n (Patience)"
$STD npm install -g n8n@latest
# n8n-nodes-base 2.46.0 pins both @napi-rs/canvas musl binaries as hard
# dependencies, which no os/cpu/libc combination can satisfy. Drop --force
# once upstream makes them optional again.
$STD npm install -g n8n@latest --force
msg_ok "Installed n8n"
msg_info "Creating Service"
+7 -1
View File
@@ -63,7 +63,13 @@ setcap cap_net_raw+p /bin/ping
msg_ok "Built Nagios Plugins"
msg_info "Configuring Web Authentication"
$STD htpasswd -bc /usr/local/nagios/etc/htpasswd.users nagiosadmin nagiosadmin
NAGIOS_PASS=$(random_password 20)
$STD htpasswd -bc /usr/local/nagios/etc/htpasswd.users nagiosadmin "$NAGIOS_PASS"
{
echo "Nagios Credentials"
echo "Username: nagiosadmin"
echo "Password: ${NAGIOS_PASS}"
} >~/nagios.creds
chown root:www-data /usr/local/nagios/etc/htpasswd.users
chmod 640 /usr/local/nagios/etc/htpasswd.users
msg_ok "Configured Web Authentication"
+9 -2
View File
@@ -201,6 +201,13 @@ cat <<'EOF' >/etc/neko/openbox.xml
</openbox_config>
EOF
NEKO_ADMIN_PASS=$(random_password 20)
NEKO_USER_PASS=$(random_password 20)
{
echo "Neko Credentials"
echo "Admin password: ${NEKO_ADMIN_PASS}"
echo "User password: ${NEKO_USER_PASS}"
} >~/neko.creds
cat <<EOF >/etc/neko/neko.yaml
server:
bind: "0.0.0.0:8080"
@@ -219,8 +226,8 @@ desktop:
member:
provider: "multiuser"
multiuser:
admin_password: "admin"
user_password: "neko"
admin_password: "${NEKO_ADMIN_PASS}"
user_password: "${NEKO_USER_PASS}"
EOF
msg_ok "Set up Runtime"
+1
View File
@@ -33,6 +33,7 @@ LOCAL_ENGINE_TOKEN=${LOCAL_ENGINE_TOKEN}
ENCRYPTION_KEY=${ENCRYPTION_KEY}
EOF
chmod 0640 /etc/nexterm-engine/config.yaml /etc/nexterm-server/server.env
echo "x /tmp/nexterm-sea-*" >/etc/tmpfiles.d/nexterm.conf
msg_ok "Configured Nexterm"
msg_info "Creating Services"
+5 -2
View File
@@ -31,6 +31,7 @@ NODEBB_USER="nodebb"
NODEBB_PWD="$(random_password 13)"
MONGO_CONNECTION_STRING="mongodb://${NODEBB_USER}:${NODEBB_PWD}@localhost:27017/nodebb"
NODEBB_SECRET=$(uuidgen)
NODEBB_ADMIN_PASS="$(random_password 20)"
cat <<EOF >~/nodebb.creds
NodeBB-Credentials
Mongo Database User: $MONGO_ADMIN_USER
@@ -38,6 +39,8 @@ Mongo Database Password: $MONGO_ADMIN_PWD
NodeBB User: $NODEBB_USER
NodeBB Password: $NODEBB_PWD
NodeBB Secret: $NODEBB_SECRET
NodeBB Admin User: community-scripts
NodeBB Admin Password: $NODEBB_ADMIN_PASS
EOF
$STD mongosh <<EOF
@@ -97,10 +100,10 @@ expect "Administrator email address" {
send "admin@community-scripts.org\r"
}
expect "Password" {
send "community-scripts\r"
send "$NODEBB_ADMIN_PASS\r"
}
expect "Confirm Password" {
send "community-scripts\r"
send "$NODEBB_ADMIN_PASS\r"
}
expect eof
EOF
+7 -1
View File
@@ -57,11 +57,17 @@ done
ACME_EMAIL_INPUT="${var_npmplus_acme_email:-}"
[[ -n "$ACME_EMAIL_INPUT" ]] || read -r -p "${TAB3}Enter your ACME Email (optional, press ENTER to skip): " ACME_EMAIL_INPUT || true
ADMIN_PASS=$(random_password 20)
yq -i "
.services.npmplus.environment |=
(map(select(. != \"TZ=*\" and . != \"ACME_EMAIL=*\" and . != \"INITIAL_ADMIN_EMAIL=*\" and . != \"INITIAL_ADMIN_PASSWORD=*\")) +
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=community-scripts.org\"])
[\"TZ=$TZ_INPUT\", \"ACME_EMAIL=$ACME_EMAIL_INPUT\", \"INITIAL_ADMIN_EMAIL=admin@local.com\", \"INITIAL_ADMIN_PASSWORD=${ADMIN_PASS}\"])
" /opt/compose.yaml
{
echo "NPMplus Credentials"
echo "Email: admin@local.com"
echo "Password: ${ADMIN_PASS}"
} >~/npmplus.creds
msg_info "Building and Starting NPMplus (Patience)"
$STD docker compose up -d
+7 -1
View File
@@ -30,7 +30,13 @@ rm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED
msg_ok "Setup Python3"
msg_info "Creating user octoprint"
useradd -m -s /bin/bash -p $(openssl passwd -1 octoprint) octoprint
OCTOPRINT_PASS=$(random_password 20)
useradd -m -s /bin/bash -p "$(openssl passwd -6 "$OCTOPRINT_PASS")" octoprint
{
echo "OctoPrint Linux user Credentials"
echo "Username: octoprint"
echo "Password: ${OCTOPRINT_PASS}"
} >~/octoprint.creds
usermod -aG sudo,tty,dialout octoprint
chown -R octoprint:octoprint /opt
echo "octoprint ALL=NOPASSWD: $(command -v systemctl) restart octoprint, $(command -v reboot), $(command -v poweroff)" >/etc/sudoers.d/octoprint
+2 -3
View File
@@ -16,7 +16,7 @@ update_os
msg_info "Installing OpenObserve"
mkdir -p /opt/openobserve/data
RELEASE=$(get_latest_github_release "openobserve/openobserve")
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve
tar zxf <(curl -fsSL https://downloads.openobserve.ai/releases/openobserve/v$RELEASE/openobserve-v$RELEASE-linux-$(arch_resolve).tar.gz) -C /opt/openobserve --no-same-owner
ROOT_PASS="$(random_password 9)Aa1!"
cat <<EOF >/opt/openobserve/data/.env
@@ -25,7 +25,7 @@ ZO_ROOT_USER_PASSWORD = "${ROOT_PASS}"
ZO_DATA_DIR = "/opt/openobserve/data"
ZO_HTTP_PORT = "5080"
EOF
echo "${RELEASE}" >>~/.openobserve
echo "${RELEASE}" >~/.openobserve
msg_ok "Installed OpenObserve"
msg_info "Creating Service"
@@ -38,7 +38,6 @@ After=network.target
Type=simple
EnvironmentFile=/opt/openobserve/data/.env
ExecStart=/opt/openobserve/openobserve
ExecStop=killall -QUIT openobserve
Restart=on-failure
[Install]
+3
View File
@@ -70,6 +70,9 @@ memcached/autoinstall install
openproject/admin_email admin@example.net
openproject/default_language en
EOF
# Debian's memcached also listens on ::1 and does not start when IPv6 is disabled
$STD apt install -y memcached
sed -i '/^-l ::1$/d' /etc/memcached.conf
$STD sudo openproject configure
systemctl stop openproject-web-1
if ! grep -qF 'Environment=LD_PRELOAD=/usr/local/lib/libjemalloc.so.2' /etc/systemd/system/openproject-web-1.service; then
+9 -73
View File
@@ -13,100 +13,36 @@ setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
build-essential
msg_ok "Installed Dependencies"
msg_info "Installing Python3"
$STD apt install -y \
python3-pip \
python3-dev \
python3-venv
mkdir -p ~/.config/pip
cat >~/.config/pip/pip.conf <<EOF
[global]
break-system-packages = true
EOF
msg_ok "Installed Python3"
NODE_VERSION="22" setup_nodejs
fetch_and_deploy_gh_release "paperless-ai" "clusterzx/paperless-ai" "tarball"
msg_info "Setup Paperless-AI"
cd /opt/paperless-ai
$STD python3 -m venv /opt/paperless-ai/venv
source /opt/paperless-ai/venv/bin/activate
# TMPDIR to use container disk instead of tmpfs for large pip downloads (https://github.com/community-scripts/ProxmoxVE/issues/10338)
export TMPDIR=/opt/paperless-ai/tmp
mkdir -p "$TMPDIR"
$STD pip install --upgrade pip
$STD pip install --no-cache-dir -r requirements.txt
rm -rf "$TMPDIR"
mkdir -p data/chromadb
$STD npm ci --only=production
export ONNXRUNTIME_NODE_INSTALL=skip PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
$STD npm ci --ignore-scripts --no-audit --no-fund
$STD npm run build
$STD npm prune --omit=dev --ignore-scripts --no-audit --no-fund
mkdir -p /opt/paperless-ai/data
cat <<EOF >/opt/paperless-ai/data/.env
PAPERLESS_API_URL=
PAPERLESS_API_TOKEN=
PAPERLESS_USERNAME=
AI_PROVIDER=openai
OPENAI_API_KEY=
OPENAI_MODEL=gpt-4o-mini
OLLAMA_API_URL=
OLLAMA_MODEL=
SCAN_INTERVAL=*/10 * * * *
SYSTEM_PROMPT=""
PROCESS_PREDEFINED_DOCUMENTS=no
TAGS=
ADD_AI_PROCESSED_TAG=no
AI_PROCESSED_TAG_NAME=ki-gen
USE_PROMPT_TAGS=no
PROMPT_TAGS=
USE_EXISTING_DATA=no
API_KEY=
CUSTOM_API_KEY=
CUSTOM_BASE_URL=
CUSTOM_MODEL=
RAG_SERVICE_URL=http://localhost:8000
RAG_SERVICE_ENABLED=true
EOF
msg_ok "Setup Paperless-AI"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/paperless-ai.service
[Unit]
Description=PaperlessAI Service
After=network.target paperless-rag.service
Requires=paperless-rag.service
[Service]
WorkingDirectory=/opt/paperless-ai
Environment="NODE_ENV=production"
EnvironmentFile=/opt/paperless-ai/data/.env
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
cat <<EOF >/etc/systemd/system/paperless-rag.service
[Unit]
Description=PaperlessAI-RAG Service
After=network.target
[Service]
WorkingDirectory=/opt/paperless-ai
EnvironmentFile=/opt/paperless-ai/data/.env
ExecStart=/opt/paperless-ai/venv/bin/python3 main.py --host 0.0.0.0 --port 8000 --initialize
Environment="NODE_ENV=production"
Environment="PAPERLESS_AI_PORT=3000"
Environment="PAPERLESS_AI_DATA_DIR=/opt/paperless-ai/data"
Environment="HF_HUB_DISABLE_TELEMETRY=1"
ExecStart=/usr/bin/node dist/server/index.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now paperless-rag
sleep 5
systemctl enable -q --now paperless-ai
msg_ok "Created Service"
+7 -1
View File
@@ -43,7 +43,13 @@ $STD php artisan migrate --force --seed
msg_ok "Set up database"
msg_info "Creating Admin User"
$STD php artisan app:user:create paymenter admin admin@paymenter.org paymenter 1 -q
PAYMENTER_PASS=$(random_password 20)
$STD php artisan app:user:create paymenter admin admin@paymenter.org "$PAYMENTER_PASS" 1 -q
{
echo "Paymenter Credentials"
echo "Email: admin@paymenter.org"
echo "Password: ${PAYMENTER_PASS}"
} >~/paymenter.creds
msg_ok "Created Admin User"
msg_info "Configuring Nginx"
+8 -2
View File
@@ -46,10 +46,16 @@ tar -xzf /tmp/libheif.tar.gz -C /usr/local
ldconfig
echo "${LIBHEIF_URL}" >~/.photoprism_libheif
chmod -R 755 /opt/photoprism/photos/originals
ADMIN_PASS=$(random_password 20)
{
echo "PhotoPrism Credentials"
echo "Username: admin"
echo "Password: ${ADMIN_PASS}"
} >~/photoprism.creds
cat <<EOF >/opt/photoprism/config/.env
# Authentication
PHOTOPRISM_ADMIN_USER='admin'
PHOTOPRISM_ADMIN_PASSWORD='changeme'
PHOTOPRISM_ADMIN_PASSWORD='${ADMIN_PASS}'
PHOTOPRISM_AUTH_MODE='password'
PHOTOPRISM_PUBLIC='false'
@@ -112,7 +118,7 @@ StoragePath: "/opt/photoprism/storage"
OriginalsPath: "/opt/photoprism/photos/originals"
ImportPath: "/media"
AdminUser: "admin"
AdminPassword: "changeme"
AdminPassword: "${ADMIN_PASS}"
AuthMode: "password"
DatabaseDriver: "sqlite"
HttpHost: "0.0.0.0"
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://pixelfed.org/
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
nginx \
redis-server \
ffmpeg \
jpegoptim \
optipng \
pngquant \
gifsicle \
libvips42
msg_ok "Installed Dependencies"
msg_info "Creating Pixelfed User"
useradd -rU -s /bin/bash pixelfed
usermod -aG redis pixelfed
msg_ok "Created Pixelfed User"
PG_VERSION="17" setup_postgresql
PG_DB_NAME="pixelfed" PG_DB_USER="pixelfed" setup_postgresql_db
PHP_VERSION="8.4" PHP_FPM="YES" PHP_MODULE="bcmath,ctype,curl,exif,gd,imagick,intl,mbstring,pgsql,redis,xml,zip" PHP_UPLOAD_MAX_FILESIZE="500M" PHP_POST_MAX_SIZE="500M" PHP_MAX_EXECUTION_TIME="600" setup_php
setup_composer
msg_info "Configuring Redis"
REDIS_PASS=$(random_password)
sed -i "s/^# requirepass foobared/requirepass $REDIS_PASS/" /etc/redis/redis.conf
sed -i "s/^requirepass .*/requirepass $REDIS_PASS/" /etc/redis/redis.conf
systemctl restart redis-server
msg_ok "Configured Redis"
msg_info "Configuring PHP-FPM Pool"
cp /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's/\[www\]/[pixelfed]/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's/^user = www-data/user = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's/^group = www-data/group = pixelfed/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's|^listen = .*|listen = /run/php/php8.4-fpm-pixelfed.sock|' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's/^listen.owner = .*/listen.owner = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i 's/^listen.group = .*/listen.group = www-data/' /etc/php/8.4/fpm/pool.d/pixelfed.conf
sed -i -E 's/^\s*;?\s*ffi\.enable\s*=.*/ffi.enable=true/' /etc/php/8.4/fpm/php.ini
systemctl restart php8.4-fpm
msg_ok "Configured PHP-FPM Pool"
fetch_and_deploy_gh_release "pixelfed" "pixelfed/pixelfed" "tarball" "latest" "/opt/pixelfed"
msg_info "Installing Pixelfed (Patience)"
cd /opt/pixelfed
cat <<EOF >/opt/pixelfed/.env
APP_NAME="Pixelfed"
APP_ENV="production"
APP_KEY=
APP_DEBUG="false"
APP_URL=http://${LOCAL_IP}
APP_DOMAIN=${LOCAL_IP}
ADMIN_DOMAIN=${LOCAL_IP}
SESSION_DOMAIN=null
TRUST_PROXIES="*"
FORCE_HTTPS_URLS="false"
OPEN_REGISTRATION="false"
ENFORCE_EMAIL_VERIFICATION="false"
PF_MAX_USERS="1000"
OAUTH_ENABLED="true"
ENABLE_CONFIG_CACHE="true"
INSTANCE_DISCOVER_PUBLIC="true"
PF_OPTIMIZE_IMAGES="true"
IMAGE_QUALITY="80"
MAX_PHOTO_SIZE="15000"
MAX_CAPTION_LENGTH="500"
MAX_ALBUM_LENGTH="4"
DB_CONNECTION="pgsql"
DB_HOST="127.0.0.1"
DB_PORT="5432"
DB_DATABASE="${PG_DB_NAME}"
DB_USERNAME="${PG_DB_USER}"
DB_PASSWORD="${PG_DB_PASS}"
REDIS_CLIENT="predis"
REDIS_SCHEME="tcp"
REDIS_HOST="127.0.0.1"
REDIS_PASSWORD="${REDIS_PASS}"
REDIS_PORT="6379"
SESSION_DRIVER="database"
CACHE_STORE="redis"
QUEUE_DRIVER="redis"
BROADCAST_DRIVER="log"
LOG_CHANNEL="stack"
HORIZON_PREFIX="horizon-"
ACTIVITY_PUB="true"
AP_REMOTE_FOLLOW="true"
AP_INBOX="true"
AP_OUTBOX="true"
AP_SHAREDINBOX="true"
EXP_EMC="true"
MAIL_DRIVER="log"
MAIL_HOST="smtp.mailtrap.io"
MAIL_PORT="2525"
MAIL_USERNAME="null"
MAIL_PASSWORD="null"
MAIL_ENCRYPTION="null"
MAIL_FROM_ADDRESS="pixelfed@example.com"
MAIL_FROM_NAME="Pixelfed"
PF_ENABLE_CLOUD="false"
FILESYSTEM_CLOUD="s3"
SESSION_SECURE_COOKIE="false"
HTTPS="false"
EOF
chmod -R 755 /opt/pixelfed
chmod -R 775 /opt/pixelfed/storage /opt/pixelfed/bootstrap/cache
export COMPOSER_ALLOW_SUPERUSER=1
$STD composer install --no-dev --no-ansi --no-interaction --optimize-autoloader
$STD php artisan key:generate --force
$STD php artisan storage:link
$STD php artisan migrate --force
$STD php artisan import:cities
$STD php artisan passport:keys
$STD php artisan route:cache
$STD php artisan view:cache
$STD php artisan config:cache
$STD php artisan instance:actor
$STD php artisan horizon:install
chown -R pixelfed:pixelfed /opt/pixelfed
msg_ok "Installed Pixelfed"
msg_info "Configuring Nginx"
cat <<'EOF' >/etc/nginx/sites-available/pixelfed
server {
listen 80;
server_name _;
root /opt/pixelfed/public;
index index.php;
charset utf-8;
client_max_body_size 100M;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php8.4-fpm-pixelfed.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
EOF
nginx_enable_site pixelfed
msg_ok "Configured Nginx"
msg_info "Creating Services"
cat <<'EOF' >/etc/systemd/system/pixelfed-horizon.service
[Unit]
Description=Pixelfed Horizon Queue Worker
After=network.target redis-server.service postgresql.service
Requires=redis-server.service
[Service]
Type=simple
User=pixelfed
WorkingDirectory=/opt/pixelfed
ExecStart=/usr/bin/php /opt/pixelfed/artisan horizon
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
cat <<'EOF' >/etc/systemd/system/pixelfed-scheduler.service
[Unit]
Description=Pixelfed Task Scheduler
After=network.target
[Service]
Type=oneshot
User=pixelfed
WorkingDirectory=/opt/pixelfed
ExecStart=/usr/bin/php /opt/pixelfed/artisan schedule:run
EOF
cat <<'EOF' >/etc/systemd/system/pixelfed-scheduler.timer
[Unit]
Description=Run Pixelfed Scheduler every minute
[Timer]
OnCalendar=*-*-* *:*:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl enable -q --now pixelfed-horizon pixelfed-scheduler.timer
msg_ok "Created Services"
motd_ssh
customize
cleanup_lxc
+3 -1
View File
@@ -70,7 +70,9 @@ After=network.target
[Service]
WorkingDirectory=/opt/planka
ExecStart=/usr/bin/npm start --prod
Environment=NODE_ENV=production
Environment=HOME=/root
ExecStart=/usr/bin/node app.js --prod
Restart=always
[Install]
+4 -2
View File
@@ -3,7 +3,7 @@
# Copyright (c) 2021-2026 tteck
# Author: tteck (tteckster)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://pocketbase.io/ | Github: https://github.com/pocketbase/pocketbase
# Source: https://pocketbase.io/ | Repo: https://ganigeorgiev.com/pocketbase/pocketbase
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
@@ -13,7 +13,9 @@ setting_up_container
network_check
update_os
fetch_and_deploy_gh_release "pocketbase" "pocketbase/pocketbase" "prebuild" "latest" "/opt/pocketbase" "pocketbase*linux_$(arch_resolve).zip"
RELEASE=$(curl_with_retry "https://ganigeorgiev.com/api/collections/releases/records?filter=channel.repository.name%3D%22pocketbase%22%20%26%26%20channel.name%3D%22default%22&sort=-semver&skipTotal=1&perPage=1" "-" | jq -r '.items[0].tag' | sed 's/^v//')
fetch_and_deploy_from_url "https://ganigeorgiev.com/meta/pocketbase/pocketbase/download/v${RELEASE}/pocketbase_${RELEASE}_linux_$(arch_resolve).zip" "/opt/pocketbase"
echo "${RELEASE}" >~/.pocketbase
msg_info "Configuring Pocketbase"
mkdir -p /opt/pocketbase/{pb_public,pb_migrations,pb_hooks}
+4
View File
@@ -48,6 +48,10 @@ msg_ok "Installed Podman"
mkdir -p /etc/containers/systemd
if [[ ! -x /usr/libexec/podman/quadlet ]]; then
msg_warn "Portainer runs as a Quadlet unit, which needs Podman 4.4 or newer (Debian 13); skipping it"
var_podman_portainer="n" var_podman_portainer_agent="n"
fi
prompt="${var_podman_portainer:-}"
[[ -n "$prompt" ]] || read -r -p "${TAB3}Would you like to add Portainer? <y/N> " prompt || true
if [[ ${prompt,,} =~ ^(y|yes)$ ]]; then
+2 -2
View File
@@ -16,9 +16,9 @@ update_os
msg_info "Installing Proxmox Mail Gateway"
setup_deb822_repo \
"pmg" \
"https://enterprise.proxmox.com/debian/proxmox-release-trixie.gpg" \
"https://enterprise.proxmox.com/debian/proxmox-release-$(get_os_info codename).gpg" \
"http://download.proxmox.com/debian/pmg" \
"trixie" \
"$(get_os_info codename)" \
"pmg-no-subscription"
$STD apt install -y proxmox-mailgateway-container
msg_ok "Installed Proxmox Mail Gateway"
+9 -1
View File
@@ -18,12 +18,20 @@ fetch_and_deploy_gh_release "qbittorrent" "userdocs/qbittorrent-nox-static" "sin
msg_info "Setup qBittorrent-nox"
mv /opt/qbittorrent/qbittorrent /opt/qbittorrent/qbittorrent-nox
mkdir -p ~/.config/qBittorrent/
QB_PASS=$(random_password 20)
QB_SALT=$(random_password 16)
QB_HASH="$(printf '%s' "$QB_SALT" | base64 -w0):$(openssl kdf -keylen 64 -kdfopt digest:SHA512 -kdfopt pass:"$QB_PASS" -kdfopt salt:"$QB_SALT" -kdfopt iter:100000 -binary PBKDF2 | base64 -w0)"
{
echo "qBittorrent Credentials"
echo "Username: admin"
echo "Password: ${QB_PASS}"
} >~/qbittorrent.creds
cat <<EOF >~/.config/qBittorrent/qBittorrent.conf
[LegalNotice]
Accepted=true
[Preferences]
WebUI\Password_PBKDF2="@ByteArray(amjeuVrF3xRbgzqWQmes5A==:XK3/Ra9jUmqUc4RwzCtrhrkQIcYczBl90DJw2rT8DFVTss4nxpoRhvyxhCf87ahVE3SzD8K9lyPdpyUCfmVsUg==)"
WebUI\Password_PBKDF2="@ByteArray(${QB_HASH})"
WebUI\Port=8090
WebUI\UseUPnP=false
WebUI\Username=admin
+7 -1
View File
@@ -43,9 +43,15 @@ $STD rabbitmqctl enable_feature_flag all
msg_ok "Enabled RabbitMQ Management Plugin"
msg_info "Creating User"
$STD rabbitmqctl add_user proxmox proxmox
RABBITMQ_PASS=$(random_password 20)
$STD rabbitmqctl add_user proxmox "$RABBITMQ_PASS"
$STD rabbitmqctl set_user_tags proxmox administrator
$STD rabbitmqctl set_permissions -p / proxmox ".*" ".*" ".*"
{
echo "RabbitMQ Credentials"
echo "Username: proxmox"
echo "Password: ${RABBITMQ_PASS}"
} >~/rabbitmq.creds
msg_ok "Created User"
motd_ssh
+19 -8
View File
@@ -257,15 +257,33 @@ map $http_upgrade $connection_upgrade {
'' close;
}
# COEP/COOP for the EmulatorJS and js-dos players (SharedArrayBuffer needs a
# crossOriginIsolated document). Matched on $request_uri because try_files
# rewrites $uri to /index.html before add_header runs, so a per-location
# add_header on the player route never fires. Mirrors upstream RomM's
# docker/nginx/templates/default.conf.template.
map $request_uri $coep_header {
default "";
~^/rom/.*/(ejs|jsdos)(\?|$) "require-corp";
}
map $request_uri $coop_header {
default "";
~^/rom/.*/(ejs|jsdos)(\?|$) "same-origin";
}
server {
listen 80;
server_name _;
root /opt/romm/frontend/dist;
client_max_body_size 0;
# Frontend SPA
# Frontend SPA - also emits the player isolation headers: the player URLs
# fall through try_files to /index.html, which this location serves
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
add_header Cross-Origin-Embedder-Policy $coep_header;
add_header Cross-Origin-Opener-Policy $coop_header;
}
# Static assets
@@ -276,13 +294,6 @@ server {
add_header Cache-Control "public, immutable";
}
# EmulatorJS player - requires COOP/COEP headers for SharedArrayBuffer
location ~ ^/rom/.*/ejs$ {
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Opener-Policy "same-origin";
try_files $uri /index.html;
}
# Backend API
location /api {
proxy_pass http://romm_backend;
+1 -1
View File
@@ -151,7 +151,7 @@ INVITE_EMAIL_TEMPLATE=/opt/signoz/templates/invitation_email_template.html
SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
SIGNOZ_WEB_ENABLED=true
SIGNOZ_WEB_DIRECTORY=/opt/signoz/web
SIGNOZ_JWT_SECRET=secret
SIGNOZ_JWT_SECRET=$(openssl rand -hex 32)
SIGNOZ_ALERTMANAGER_PROVIDER=signoz
SIGNOZ_TELEMETRYSTORE_PROVIDER=clickhouse
SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://localhost:9000?password=
+7 -1
View File
@@ -78,6 +78,12 @@ msg_info "Configuring SnapOtter"
mkdir -p /opt/snapotter_data/files
mkdir -p /tmp/snapotter-workspace
SNAPOTTER_PASS=$(random_password 20)
{
echo "SnapOtter Credentials"
echo "Username: admin"
echo "Password: ${SNAPOTTER_PASS}"
} >~/snapotter.creds
cat <<EOF >/opt/snapotter_data/.env
PORT=1349
NODE_ENV=production
@@ -92,7 +98,7 @@ FEATURE_MANIFEST_PATH=/opt/snapotter/docker/feature-manifest.json
U2NET_HOME=/opt/snapotter_data/ai/models/rembg
AUTH_ENABLED=true
DEFAULT_USERNAME=admin
DEFAULT_PASSWORD=admin
DEFAULT_PASSWORD=${SNAPOTTER_PASS}
LOG_LEVEL=info
TRUST_PROXY=true
FILE_MAX_AGE_HOURS=72
-4
View File
@@ -116,10 +116,6 @@ if [[ "$login_mode" == "true" ]]; then
# activate Login
DISABLE_ADDITIONAL_FEATURES=false
SECURITY_ENABLELOGIN=true
# login credentials
SECURITY_INITIALLOGIN_USERNAME=admin
SECURITY_INITIALLOGIN_PASSWORD=stirling
EOF
fi
msg_ok "Created Environment Variables"
+6 -4
View File
@@ -17,7 +17,8 @@ msg_info "Installing Dependencies"
$STD apt install -y \
build-essential \
python3 \
git
git \
redis-server
msg_ok "Installed Dependencies"
NODE_VERSION="24" NODE_MODULE="pnpm" setup_nodejs
@@ -56,8 +57,8 @@ SECRET_KEY=${SECRET_KEY}
PORT=3000
NODE_ENV=production
NEXT_TELEMETRY_DISABLED=1
BACKEND_CACHE_PROVIDER=sqlite
BACKEND_CACHE_SQLITE_URI=sqlite:///opt/teable/.assets/.cache.db
BACKEND_CACHE_PROVIDER=redis
BACKEND_CACHE_REDIS_URI=redis://127.0.0.1:6379/0
NEXTJS_DIR=apps/nextjs-app
EOF
ln -sf /opt/teable /app
@@ -70,10 +71,11 @@ fi
msg_ok "Configured Teable"
msg_info "Creating Service"
systemctl enable -q --now redis-server
cat <<EOF >/etc/systemd/system/teable.service
[Unit]
Description=Teable
After=network.target postgresql.service
After=network.target postgresql.service redis-server.service
[Service]
Type=simple
+1 -1
View File
@@ -23,7 +23,7 @@ setup_deb822_repo \
$STD apt install -y aspnetcore-runtime-10.0
msg_ok "Installed Dependencies"
RELEASE=$(curl -fsSL https://technitium.com/dns/ | grep -oP 'Version \K[\d.]+')
RELEASE=$(get_latest_github_release "TechnitiumSoftware/DnsServer")
fetch_and_deploy_from_url "https://download.technitium.com/dns/DnsServerPortable.tar.gz" /opt/technitium/dns
echo "${RELEASE}" >~/.technitium
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff. Show more