Thanks for helping. Each top-level folder is a standalone script for Plesk servers, so changes are usually small and local. Terms such as subscription and gated update are defined in GLOSSARY.md; design decisions are in docs/adr.
- Fork the repository and create a branch.
- Make your change, lint it, and update the docs (see Documentation).
- Open a pull request.
There is no build step and no automated test suite for the data-handling scripts. Test in a staging Plesk environment before merging.
- Create a top-level folder named after the task.
- Add a
.shwith the self-update block. Add a.batonly if the task needs Windows (ADR 0002). - Add a
README.mdto the folder using the template in Documentation. - Add a row to the Scripts table in the root
README.md. - Add the script to the maintenance matrix and the PID lock /
umask 077table in AGENTS.md. - Run
shellcheckon the new script.
@echo off
setlocal enabledelayedexpansion
REM Description: Brief purpose of the script-
Use delayed expansion (
!variable!, not%variable%) so paths with parentheses work. -
Quote every path:
"!VARIABLE!". -
Use
usebackqinFOR /Floops:for /F "usebackq tokens=*" %%i in ("!FILE!"). -
Test with paths containing spaces and parentheses, for example
C:\Program Files (x86)\Plesk. -
Check
plesk_dirbefore use and exit on failure:if not defined plesk_dir ( echo ERROR: plesk_dir environment variable is not set exit /b 1 )
-
Check
errorlevelafter operations that can fail.
#!/bin/bash
set -euo pipefail
# Description: Brief purpose of the script- Configure through environment variables with defaults, and validate them:
DAYS=${DAYS:-365}, then check that required directories exist. - Use LF line endings (the maintainer edits through WSL).
- Self-update block: copy the block from
.github/self-update.template.shverbatim, immediately afterset -euo pipefail, and change onlySCRIPT_RELATIVE_PATH(the script's path in the repo) andUPDATE_CHECK_FILE(unique per script). CI fails on any other difference. If you change the block, change the template first, then every script's copy. Blocks are copied per script, never shared. - PID locking is required when the script can run on a schedule and overlap itself, or writes shared state. Use a PID file and a
trap ... EXITthat removes it. Seemysql-backup.sh. umask 077is required when the script writes backups, state or reports that may be sensitive.- Never hardcode credentials. Windows scripts use the
<password_for_mysql>placeholder; Linux scripts useplesk db. - MySQL scripts always exclude
information_schema,performance_schemaandphpmyadmin. - Reuse the SMTP variable names
SMTP_SERVER,SMTP_PORT,SMTP_AUTH_USER,SMTP_AUTH_PASS,SMTP_SECUREandSMTP_FROMfor any email relay.
Every script starts with a header comment:
# Purpose: One-line description of what the script does
# Platform: Windows/Linux
# Features:
# - Feature 1
# Usage: ./script.sh [--update|--self-update] or script.bat
# Environment Variables:
# - VAR_NAME: Description (default: value)
# - AUTO_UPDATE: Set to "true" to enable automatic updates (default: false) [Linux only]
# - UPDATE_CHECK_INTERVAL: Hours between update checks (default: 24) [Linux only]
# - UPDATE_VERSION: Release tag to install on update (default: latest release) [Linux only]
# Security: Warning about credentials/placeholders if applicableBefore opening a PR:
- Lint each changed bash script:
shellcheck path/to/script.sh - Run the CI helpers locally:
.github/scripts/check-self-update.sh.github/scripts/test-self-update.sh.github/scripts/check-docs.sh
- Test in a staging or dev Plesk environment and cover the error paths: missing credentials, an empty database list, a missing directory, and concurrent runs (PID lock).
- Check that files written on Linux have restrictive permissions (600).
- Test Windows scripts with paths containing spaces and parentheses.
.github/workflows/ci.yml runs four jobs:
- shellcheck on every
.shfile .github/scripts/check-self-update.sh: every script's self-update block matches.github/self-update.template.sh,SCRIPT_RELATIVE_PATHequals the file path, andUPDATE_CHECK_FILEis unique.github/scripts/test-self-update.sh: runs the gated updater against a stubbedcurl(update, pin, checksum mismatch, no release, bad version)- actionlint on the workflows
.github/workflows/docs.yml runs .github/scripts/check-docs.sh (also on markdown-only changes): relative links resolve, every script folder has a README.md, and every env var in a script's header "Environment Variables" block appears in backticks in that README.
Never tag by hand. Add a CHANGELOG.md entry under [Unreleased] for any script change users should pick up. On pushes to main that have unreleased entries, release-pr.yml opens or updates a "Release vYYYY.MM.DD" PR that renames [Unreleased]. Merging it triggers release.yml, which re-runs shellcheck and the drift check, then creates the tag and release with SHA256SUMS. This needs the repository setting Actions → Allow GitHub Actions to create pull requests.
Docs-only changes need no changelog entry.
Each script's folder README.md is the user documentation for that script. The root README.md is an index plus the shared install, self-update and security notes. Update the folder README in the same PR as any script change; the maintenance matrix in AGENTS.md lists what else to update.
Folder README template:
- Purpose
- Platform (and which version is a subset, if any)
- Prerequisites
- Usage
- Configuration (environment variables as a table with defaults, plus command-line options)
- Scheduling
- Troubleshooting
List every header env var in the README table (CI enforces this). Update GLOSSARY.md when you introduce or change a domain term, and add an ADR in docs/adr/ only for decisions that are hard to reverse, surprising without context, and the result of a real trade-off.