Contributing a script
The path a new script takes from request to ProxmoxVE, and how to fix one that is already there.
New scripts are written and tested in DevScripts and move to ProxmoxVE once they have been tested. Fixes to a script that already lives in ProxmoxVE go straight to ProxmoxVE.
| I want to | Where |
|---|---|
| Ask for a script I will not write myself | ProxmoxVE Discussions, category "request-script" |
| Add a new script | Pull request against DevScripts |
| Fix or improve a script that is in ProxmoxVE | Pull request against ProxmoxVE |
| Fix a script that is still in DevScripts | Pull request against DevScripts |
| Change the engine (menus, helpers, container creation) | Pull request against core, see its contributing guide |
| Report a security problem | Privately, as described in SECURITY.md |
A new script, step by step
1. Check that the application is eligible
The DevScripts pull request template lists the requirements. All of them have to be ticked for a new script:
- the application is at least 6 months old
- it is actively maintained
- it has 1,000+ GitHub stars
- official release tarballs are published
The last box in that list says that not every script is accepted, whatever the numbers are.
Two more rules come from AGENTS.md and the template's review section:
- The application is installed bare-metal. An application that can only run in Docker is not accepted as a
ct/script. - Updates use the release helpers (
fetch_and_deploy_gh_releaseand its siblings), notgit pull.
If you only want to request a script, open a discussion in the request-script category. A workflow (request-script-eligibility.yml) reads the "Source Repository" field. For a github.com URL it looks up the star count and the repository's creation date:
| Result | What happens |
|---|---|
| 1,000 stars or more and 6 months or older | Nothing, the request stays open |
| Below that, or not verifiable, and "Justification for an exception" is filled in | Label investigation, a maintainer decides |
| Below that, or not verifiable, and no justification | A comment explains why, the discussion is closed as outdated |
A closed request can be edited to add the justification and reopened; the check then runs again.
2. Fork DevScripts and write three files
Fork DevScripts, create a branch, and add:
| File | Purpose | Template |
|---|---|---|
ct/<slug>.sh | Defaults, update_script, container creation. Runs on the host. | CT template |
install/<slug>-install.sh | Installs the application. Runs inside the container. | Install template |
json/<slug>.json | Website metadata. | JSON template |
The slug is lowercase with hyphens and is the same in all three file names. The coding rules say what reviewers expect inside the files.
DevScripts also takes vm/, tools/addon/ and tools/pve/ scripts. The rest of this page follows a container script.
3. Test it yourself
Run the script from your branch on a real host before opening the pull request. Testing from a fork has the commands. Test the install and, once it is installed, update inside the container.
4. Open the pull request
Open it against main of DevScripts and fill in the template. Keep the description to two to five sentences: what the application is, what deviates from the standard pattern, how you tested.
A workflow (unmet-pr-close.yml) reads the description on every edit and push. It complains when:
- the "Application Requirements" section is missing, which means the template was not used
- the "AI Assistance" section has neither "No AI used" ticked nor a sentence saying how an LLM was used
- "New script" is ticked and one of these is not: the four application requirements, the "I understand that not all scripts will be accepted" box, one of the three arm64 options, one of the two AI options, "No Docker / Docker Compose", "No git pull"
When something is missing, the pull request gets a comment listing it and the labels requirements missing and stale. Edit the description; the check runs again and removes both labels. A pull request that still has them after 7 days is closed by stale_pr_close.yml. A maintainer can set keep-open to stop that.
If the pull request adds files under ct/ or install/ and "New script" is not ticked, you get a one-time reminder comment.
5. Automated checks
Three workflows react to a pull request that touches scripts:
| Workflow | What it does |
|---|---|
| Bash Syntax Check | Runs bash -n over every .sh file in ct/, install/, vm/ and tools/ |
| PR test command | Posts a comment with a ready-to-paste command for each changed script, so reviewers can try your branch |
| PR script test | Builds the changed scripts on real hosts and reports the result, see below |
6. The script test
The test runs on every push to a pull request that is not a draft and changes ct/, install/ or vm/. A changed install/<slug>-install.sh is tested through ct/<slug>.sh. Scripts under tools/ are not built. One run builds at most 10 scripts.
Anyone with write access can start it again with a comment:
| Comment | Effect |
|---|---|
/test | Build everything the pull request touches, on every platform |
/test arm or /test pve incus | Build only on the named platforms (pve, incus, arm). Such a run gets its own check name and never clears an earlier failure. |
/test bark-server | Build only that script |
/test followed by a full 40-character commit SHA | Build exactly that commit, as long as it is still the head |
Where it builds
| Platform | Host | Counts for the result |
|---|---|---|
pve | A Proxmox VE test host | Yes |
incus | An Incus test host | Yes |
arm | Incus on a GitHub arm64 runner | No, information only |
A script that sets var_arm64 to no is not built on arm64. A script that leaves var_arm64 unset is built there with var_arm64=yes, to see whether it would work. A script that contains msg_menu "Choose the container OS" is built once with Debian and once with Alpine. VM scripts are built on Proxmox VE only.
How it runs your script
The build is unattended: mode=default, a fixed var_ctid, var_hostname and bridge, and dev_mode=timing,keep. Nobody can answer a prompt, so every application variable gets a value up front. The names come from app_vars in json/<slug>.json and from every var_... name found on a read or prompt_... line of the install and ct/ script. A declared default is used as the value; without one the test invents a value that fits the name (a random string for names containing pass, secret, token or key, ci@example.test for email, and so on).
What it checks, in order
- The build exits 0 within 45 minutes, the install script inside the container did not fail, and the output contains "Completed successfully".
- The container is running.
- No service has failed (
systemctl --failed, orrc-status --crashedon Alpine). - The first
http://${IP}...orhttps://${IP}...address in thect/script answers with a 2xx status within 120 seconds, redirects followed. A script that prints no such address has no URL check. A screenshot of the page is attached to the result. - Update: the version files in
/rootthatupdate_scriptchecks are set back, thenupdateruns inside the container. It has to exit 0 within 30 minutes without an error line. If it prints "Updated successfully", each version file must have changed. Services and URL are checked again. - Second update, only when the first one updated: it must run cleanly a second time. Services and URL are checked again.
- Stability: the restart counters of all services are read, the test waits 60 seconds and reads them again. Services and URL are checked one more time.
What the findings mean
The "Detail" column of the result table shows the first thing that went wrong.
| Finding | Meaning |
|---|---|
build timed out after 45m | The ct/ script did not finish in time |
build exited N: ... | The ct/ script failed; the text after the colon is the last error line of the output |
unanswerable prompt (AGENTS.md rule 24): ... | A read fired although nobody can type. Read var_<x> first and ask only when it is unset, see the install template |
install script exited N: ... | The install script failed inside the container |
build exited 0 but never reported success | The closing msg_ok "Completed Successfully!\n" line is missing or was not reached |
container ... is not running | The container stopped after the build |
failed services: ... | The named units are in a failed state |
<url> answered 502 instead of 200 within 120s | The application did not come up, or the address in the ct/ script is wrong. "answered nothing" means no connection at all |
update: exited N, update: timed out after 30m, update: <error line> | update_script failed |
update: reported success, but /root/.<app> is still ... | update_script printed "Updated successfully" but the version file was not rewritten, usually because the deploy helper was not called |
update: after the update: ... | The update ran, but a service failed or the URL stopped answering afterwards |
update: second run ... | A second update right after the first failed |
not stable: restarted within 60s without anyone touching it: <unit> (+N) | A service restarted N times while the container was left alone. It is crash-looping, even if it looks active at any single moment |
not stable: 60s later, ... | A minute after everything passed, a service had failed or the URL no longer answered |
Under the table, Notes list findings that do not fail the test:
| Note | Meaning |
|---|---|
disk ... MB used after the install: var_disk may be too small | 90 % or more of the disk is used |
... MB RAM in use at rest: var_ram may be too small | 80 % or more of the memory limit is in use while idle |
the kernel killed N process(es) for lack of memory | An out-of-memory kill happened during the build or at runtime |
a second update right after the first updated again | update_script does not notice that it is current, often a version file it never writes |
N error line(s) in the journal since boot | Error-level journal entries, with the two most frequent as examples |
A minus sign in the "Update" column means update ran without updating anything. That is not a failure: an application installed from a package manager has no version file to set back.
Result and review state
| Row status | Meaning | Effect |
|---|---|---|
| pass | Everything above passed | Counts as passed |
| fail | The script or the application broke | The check fails and the bot requests changes on that commit |
| error | The test setup broke (runner, download, rate limit), not your script | No review, the check asks for attention |
| unsupported | The script needs a CPU feature the test host lacks, for example AVX2 | Not a failure, has to be tested by hand |
| cancelled | The run was cancelled | Nothing |
Push a fix and the test runs again. A full run that passes dismisses the bot's earlier "changes requested".
7. Review
A maintainer reviews the code against the coding rules; the review checklist shows what they look for. Apply a review suggestion as written and resolve its thread. Do not regenerate the file to answer a comment.
A pull request where someone other than the author has commented or reviewed and nothing has happened for 14 days is labelled stale. A new commit removes the label. Without one it is closed 7 days later; a maintainer comment restarts those 7 days.
8. The testing phase
Merging the pull request starts the testing phase. Nothing here needs action from you except answering testers.
- pr-merged-ready-for-testing.yml looks at the
json/*.jsonfiles the pull request added. For each one it finds the open issue whose "Name of the Script" (lowercased, spaces as hyphens) equals the slug and labels itReady For Testing. If there is no issue, it opens one from the "Script" section of your pull request (name, tool, category, platforms, arm64 option). - The label publishes the metadata: push_json_to_pocketbase.yml creates the website record from
json/<slug>.json, marked as a development script. Later changes to that JSON onmainupdate the record. - The label also sets the feedback address: sync-testurl.yml commits a
var_testurlline intoct/<slug>.shthat points at the issue. It finds the script by comparing the issue title withAPP=, ignoring everything but letters and digits, so the issue title has to be the application name.
var_testurl="${var_testurl:-https://github.com/community-scripts/DevScripts/issues/2533}"That line is from ct/temporal.sh in DevScripts. With it, a container built from the script asks for feedback in four places: in the login message, in the container description in the Proxmox VE interface, through a testing tag, and on the last line of the install. The value must be a plain https:// URL of at most 200 characters; anything else is ignored with a warning. Without the line, a DevScripts container still shows the generic development warning.
According to the DevScripts README, a script under test also has a thread on the project's Discord with the command to run. Testers report in the issue or in that thread. Without feedback a script is not released.
9. Promotion to ProxmoxVE
When testing is done, a maintainer labels the issue Migration To ProxmoxVE. move-to-main-repo.yaml then:
- reads the slug from "Name of the Script" and the type from "Script Type"
- copies
ct/<slug>.shandinstall/<slug>-install.sh(or thevm/,tools/addon/,tools/pve/file) into a new ProxmoxVE branch. If a file of that name already exists in ProxmoxVE it stops and says so in the issue. - removes the
var_testurlline and the_CS_DEFAULT_URLline from thect/script and rewrites everycommunity-scripts/DevScriptsreference tocommunity-scripts/ProxmoxVE - opens a pull request in ProxmoxVE with the issue's title and body, and swaps the label for
Started Migration To ProxmoxVE
The script test runs on that pull request too. Because it was opened by the migration app on a branch of the repository, it is approved automatically when every script passes on Proxmox VE and Incus and the pull request changes nothing but ct/ and install/ scripts.
json/<slug>.json is not copied. ProxmoxVE has no json/ folder; the website record that was created in step 8 stays and is switched from "development" to released when the ProxmoxVE pull request is merged (close_issue_in_dev.yaml). The same workflow comments on the DevScripts issue and closes it, and closing it removes the script's files from DevScripts (delete_new_script.yaml).
10. After the release
From here on the script lives in ProxmoxVE:
- Fixes are pull requests against ProxmoxVE, from you or anyone else. The default reviewers for every file are the
@community-scripts/Contributorteam (CODEOWNERS). Your name stays in the script header. - Changes to
ct/andinstall/onmainare mirrored to the Incus repository by a workflow. Do not open pull requests for those folders there. - Metadata is no longer a file. Request a change through the "Report Issue" link on the script's page; organisation members apply it with the
/pocketbasecomment command.
Fixing an existing script
- Fork ProxmoxVE, create a branch, change only the files the fix needs.
- Test the change on a real host, see Testing from a fork. For a change to
update_script, test the update of a container that was installed frommain. - Open a pull request against
mainand fill in the template.
One fix or feature per pull request. A pull request that contains only the files it needs to change is reviewed faster and can be reverted on its own.
What the automation does with it:
| Check | Rule |
|---|---|
| Template | The sections "Description", "Prerequisites" and "Type of Change" must be there, the description must not be empty, all three prerequisites and at least one type must be ticked. Otherwise: a comment, the labels missing pr template and stale, closed after 7 days unless the description is fixed. Drafts, organisation members and pull requests labelled keep-open are skipped. |
| New scripts | A pull request from outside the organisation that adds a file under ct/, install/, turnkey/ or vm/ is closed with a pointer to DevScripts. |
| Script test | The same test as in DevScripts. In ProxmoxVE it adds an upgrade run when the pull request changes update_script: the script is installed from the base branch, then updated with your version, as a user who installed before the merge would. |
| Test command | A comment with the command to run your branch. |
If the change alters existing behaviour in a way users have to act on, tick "Breaking change" and add the breaking-change advisory block the template describes.
Do not send metadata changes (name, description, logo, notes) as files to ProxmoxVE; use the "Report Issue" link on the script's page.