Compare commits

..

3 Commits

Author SHA1 Message Date
push-app-to-main[bot]
5c4307fe2b Add defguard (ct) 2026-08-29 22:36:49 +00:00
community-scripts-pr-app[bot]
bddc28bb57 Update CHANGELOG.md (#16857)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-29 21:38:30 +00:00
CanbiZ (MickLesk)
ceb783d62c github action: post the command that tests a ct/ or install/ change (#16833)
* Post the command that tests a ct/ or install/ change

Reviewing a script change meant working out the URL yourself, and the
obvious guess is wrong: curling the branch URL alone gives you the ct/
script from the PR and the install/ script from main, because each script
pins _CS_DEFAULT_URL to main and that pin is what fills
COMMUNITY_SCRIPTS_URL when it is unset. Frequently the install script is
the only thing that changed.

So the comment spells out both lines, per changed app.

Only for scripts already on the core bootstrap. The older one-liner
resolves everything from ProxmoxVE/main and ignores the variable, so a
command built for it would install main and look like it passed --  worse
than no comment. Those are named instead, with what to do about them.

pull_request_target for fork PRs, and nothing from the PR is checked out
or executed: the file list and the bootstrap line come from the API, and
a branch name that is not [A-Za-z0-9._/-]+ stops the run rather than
reaching a fenced code block.

* Update .github/workflows/pr-test-command.yml

Co-authored-by: Sam Heinz <sam@samheinz.com>

---------

Co-authored-by: Sam Heinz <sam@samheinz.com>
2026-08-29 23:38:09 +02:00
5 changed files with 318 additions and 0 deletions

171
.github/workflows/pr-test-command.yml generated vendored Normal file
View File

@@ -0,0 +1,171 @@
name: PR test command
# Posts a ready-to-run test command for reviewers.
# It uses this PRs script branch with the production engine, since both resolve
# independently. Only scripts using community-scripts/core are supported.
#
# pull_request_target allows comments on fork PRs. No PR code is checked out or
# executed; API inputs are validated and the comment is assembled in JavaScript.
on:
pull_request_target:
branches: ["main"]
types: [opened, synchronize, reopened]
paths:
- "ct/**"
- "install/**"
jobs:
comment:
if: github.repository == 'community-scripts/ProxmoxVE'
runs-on: self-hosted
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/github-script@v9
with:
script: |
const MARKER = '<!-- pr-test-command -->';
const MAX_APPS = 10;
const pr = context.payload.pull_request;
const head = pr.head.repo; // null when the fork is gone
if (!head) return;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Git allows backticks in a ref name, and this one ends up inside a
// fenced block. Anything outside the ordinary set is not worth
// rendering, so bail rather than escape.
const ref = pr.head.ref;
if (!/^[A-Za-z0-9._\/-]+$/.test(ref)) return;
const base = `https://raw.githubusercontent.com/${head.full_name}/${ref}`;
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});
// ct/foo.sh and install/foo-install.sh are the same app. A removed
// file has nothing left to run.
const apps = new Map(); // slug -> {ct, install}
for (const f of files) {
if (f.status === 'removed') continue;
let m = f.filename.match(/^ct\/([a-z0-9][a-z0-9._-]*)\.sh$/);
if (m) { apps.set(m[1], { ...apps.get(m[1]), ct: true }); continue; }
m = f.filename.match(/^install\/([a-z0-9][a-z0-9._-]*)-install\.sh$/);
if (m) apps.set(m[1], { ...apps.get(m[1]), install: true });
}
if (apps.size === 0) return;
// Read the ct script at the PR head to see which engine it loads.
// Read only -- it is never sourced or run.
async function bootstrapOf(slug) {
try {
const res = await github.rest.repos.getContent({
owner: head.owner.login, repo: head.name,
path: `ct/${slug}.sh`, ref: pr.head.sha,
});
if (!res.data.content) return 'unknown';
const text = Buffer.from(res.data.content, 'base64').toString('utf8');
const firstLines = text.split('\n').slice(0, 12).join('\n');
return /_cs_boot=/.test(firstLines) ? 'core' : 'legacy';
} catch (e) {
return e.status === 404 ? 'missing' : 'unknown';
}
}
const ready = [], legacy = [], missing = [];
for (const slug of [...apps.keys()].sort()) {
const kind = await bootstrapOf(slug);
if (kind === 'core') ready.push(slug);
else if (kind === 'legacy') legacy.push(slug);
else if (kind === 'missing') missing.push(slug);
}
const lines = [MARKER];
if (ready.length > 0) {
const shown = ready.slice(0, MAX_APPS);
lines.push(
'### Try this branch',
'',
'The engine and the scripts resolve independently, so this runs the changed',
'`ct/` and `install/` scripts against the **production** engine:',
'',
);
for (const slug of shown) {
lines.push(
'```bash',
`export COMMUNITY_SCRIPTS_URL=${base}`,
`bash -c "$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/ct/${slug}.sh")"`,
'```',
'',
);
}
if (ready.length > shown.length) {
lines.push(
`${ready.length - shown.length} more script(s) changed; same command, different slug.`,
'',
);
}
lines.push(
'Both lines are needed. Each script pins `_CS_DEFAULT_URL` to `main`, and that',
'pin is what fills `COMMUNITY_SCRIPTS_URL` when the variable is unset — so',
'curling the branch URL on its own gives you the `ct/` script from this PR and',
'the `install/` script from `main`. Frequently the one you meant to test.',
'',
'The same command works on an Incus host: the engine detects the platform and',
'loads the matching backend, while the scripts still come from this branch.',
'',
'<details><summary>Useful while testing</summary>',
'',
'`dev_mode=net` logs every fetch with status and URL, which is the quickest way',
'to confirm the branch is really being used. `dev_mode=keep` stops a failed',
'build from deleting the container along with the evidence.',
'',
'```bash',
`export COMMUNITY_SCRIPTS_URL=${base}`,
`dev_mode=net,keep bash -c "$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/ct/${shown[0]}.sh")"`,
'```',
'</details>',
);
}
if (legacy.length > 0) {
lines.push(
'',
ready.length > 0 ? '---' : '### Not testable this way yet',
'',
`\`${legacy.join('`, `')}\` still uses the older one-liner bootstrap, which`,
'resolves everything from `ProxmoxVE/main` and ignores `COMMUNITY_SCRIPTS_URL`.',
'There is no way to point it at this branch — test it from a checkout on the',
'host instead, or migrate the script to the `_cs_boot` bootstrap first.',
);
}
if (missing.length > 0) {
lines.push(
'',
`No \`ct/\` script found for \`${missing.join('`, `')}\`, so there is nothing to`,
'run. If the install script was renamed, its `ct/` counterpart needs the same',
'name.',
);
}
if (lines.length === 1) return; // marker only, nothing worth saying
const body = lines.join('\n');
// Update in place rather than posting again on every push.
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
const mine = comments.find(c => c.body.includes(MARKER));
if (mine) {
if (mine.body !== body) {
await github.rest.issues.updateComment({ owner, repo, comment_id: mine.id, body });
}
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}

View File

@@ -527,6 +527,12 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-08-29
### 📚 Documentation
- github action: post the command that tests a ct/ or install/ change [@MickLesk](https://github.com/MickLesk) ([#16833](https://github.com/community-scripts/ProxmoxVE/pull/16833))
## 2026-08-28
### 🚀 Updated Scripts

57
ct/defguard.sh Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
# Copyright (c) 2021-2026 community-scripts ORG
# Author: MickLesk (CanbiZ)
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://github.com/DefGuard/defguard
APP="Defguard"
var_tags="${var_tags:-vpn;wireguard;sso}"
var_cpu="${var_cpu:-2}"
var_ram="${var_ram:-2048}"
var_disk="${var_disk:-8}"
var_os="${var_os:-debian}"
var_version="${var_version:-13}"
#var_arm64="${var_arm64:-no}" # unset = ask the user; set yes/no only when verified
var_unprivileged="${var_unprivileged:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /etc/defguard/core.conf ]]; then
msg_error "No ${APP} Installation Found!"
exit
fi
msg_info "Updating ${APP}"
$STD apt update
$STD apt install -y defguard defguard-proxy
msg_ok "Updated ${APP}"
msg_info "Restarting Services"
systemctl restart defguard defguard-proxy
msg_ok "Restarted Services"
msg_ok "Updated successfully!"
exit
}
start
build_container
description
msg_ok "Completed Successfully!\n"
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
echo -e "${INFO}${YW}Access it using the following URL:${CL}"
echo -e "${GATEWAY}${BGN}http://${IP}:8000${CL}"
echo -e "${INFO}${YW}In the setup wizard, enter this as the Edge address:${CL}"
echo -e "${TAB}${DEFAULT}${BGN}127.0.0.1:50051${CL}"
echo -e "${INFO}${YW}The generated admin password is in /etc/defguard/core.conf${CL}"

6
ct/headers/defguard Normal file
View File

@@ -0,0 +1,6 @@
____ ____ __
/ __ \___ / __/___ ___ ______ __________/ /
/ / / / _ \/ /_/ __ `/ / / / __ `/ ___/ __ /
/ /_/ / __/ __/ /_/ / /_/ / /_/ / / / /_/ /
/_____/\___/_/ \__, /\__,_/\__,_/_/ \__,_/
/____/

View File

@@ -0,0 +1,78 @@
#!/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/DefGuard/defguard
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
PG_VERSION="17" setup_postgresql
PG_DB_NAME="defguard" PG_DB_USER="defguard" setup_postgresql_db
setup_deb822_repo \
"defguard" \
"https://apt.defguard.net/defguard.asc" \
"https://apt.defguard.net" \
"$(get_os_info codename)" \
"release-2.0"
msg_info "Installing Defguard"
$STD apt install -y defguard
msg_ok "Installed Defguard"
msg_info "Configuring Defguard"
DEFGUARD_ADMIN_PASSWORD=$(openssl rand -base64 18)
cat <<EOF >/etc/defguard/core.conf
DEFGUARD_DB_HOST=localhost
DEFGUARD_DB_PORT=5432
DEFGUARD_DB_NAME=defguard
DEFGUARD_DB_USER=defguard
DEFGUARD_DB_PASSWORD=${PG_DB_PASS}
DEFGUARD_URL=http://${LOCAL_IP}:8000
DEFGUARD_HTTP_PORT=8000
DEFGUARD_GRPC_PORT=50055
DEFGUARD_DEFAULT_ADMIN_PASSWORD=${DEFGUARD_ADMIN_PASSWORD}
DEFGUARD_COOKIE_INSECURE=true
DEFGUARD_LOG_LEVEL=info
EOF
chown root:defguard /etc/defguard/core.conf
chmod 640 /etc/defguard/core.conf
systemctl restart defguard
msg_ok "Configured Defguard"
msg_info "Installing Defguard Edge"
$STD apt install -y defguard-proxy
mkdir -p /etc/defguard/certs
cat <<EOF >/etc/defguard/proxy.toml
# Defguard Edge (proxy) configuration
# Apply changes with: systemctl restart defguard-proxy
# Port the API/enrollment HTTP server listens on
http_port = 8080
# Port the HTTPS server listens on (used after Core provisions TLS)
https_port = 8443
# Port the gRPC server listens on. Core connects here to adopt and manage the Edge.
grpc_port = 50051
# Directory where adoption-provisioned mTLS certificates are stored
cert_dir = "/etc/defguard/certs"
log_level = "info"
rate_limit_per_second = 0
rate_limit_burst = 0
EOF
chown -R defguard:defguard /etc/defguard/certs /etc/defguard/proxy.toml
systemctl enable -q --now defguard-proxy
msg_ok "Installed Defguard Edge"
motd_ssh
customize
cleanup_lxc