From 63e4ea13965db4fe415e53498cb75dd35c2aaffb Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Fri, 7 Aug 2026 04:49:45 +0200 Subject: [PATCH] core: extend new vars and pocketbase values (#16313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Let docker-install.sh take its three answers up front The script asked three questions with no way to answer them in advance, so a Docker container could not be deployed unattended: Portainer, the Portainer Agent, and whether to expose the TCP socket. Each now reads a variable and prompts only when it is unset, the same shape install/forgejo-runner-install.sh already uses: var_portainer yes | no var_portainer_agent yes | no var_docker_socket n | l (127.0.0.1) | a (0.0.0.0) Interactive behaviour is unchanged — with nothing set, all three still ask exactly as before. The matching declaration for the script's PocketBase record, which the website generator reads to offer these as form fields: "app_vars": [ {"name":"var_portainer","label":"Install Portainer","type":"boolean","default":"no"}, {"name":"var_portainer_agent","label":"Install Portainer Agent","type":"boolean","default":"no", "help":"Only used when Portainer itself is not installed"}, {"name":"var_docker_socket","label":"Expose Docker TCP socket","type":"select", "options":["n","l","a"],"default":"n", "help":"l = 127.0.0.1 only, a = all interfaces (insecure)"} ] CONTRIBUTING.md documents the convention so the next script follows it. * Let docker-install.sh take its three answers up front The script asked three questions with no way to answer them in advance, so a Docker container could not be deployed unattended: Portainer, the Portainer Agent, and whether to expose the TCP socket. Each now reads a variable and prompts only when it is unset, the same shape install/forgejo-runner-install.sh already uses: var_portainer yes | no var_portainer_agent yes | no var_docker_socket n | l (127.0.0.1) | a (0.0.0.0) Interactive behaviour is unchanged — with nothing set, all three still ask exactly as before. The matching declaration for the script's PocketBase record, which the website generator reads to offer these as form fields: "app_vars": [ {"name":"var_portainer","label":"Install Portainer","type":"boolean","default":"no"}, {"name":"var_portainer_agent","label":"Install Portainer Agent","type":"boolean","default":"no", "help":"Only used when Portainer itself is not installed"}, {"name":"var_docker_socket","label":"Expose Docker TCP socket","type":"select", "options":["n","l","a"],"default":"n", "help":"l = 127.0.0.1 only, a = all interfaces (insecure)"} ] CONTRIBUTING.md documents the convention so the next script follows it. * Let forgejo-runner and pangolin take their answers up front forgejo-runner already exported two of the three values its install script requires, but not var_forgejo_runner_uuid — so an unattended install passed the ct-level guard and then stopped at a prompt inside the container, which is the one place nobody can answer it. The guard missed it for the same reason. pangolin asked for its URL and email with no way to supply them. Both sides are needed: the read in install/ now only fires when the variable is unset, and ct/ exports it, because lxc-attach carries the caller's environment but only what was exported. Reverts the docker change from the previous commit; it is superseded by work on another branch. * Teach the PocketBase workflows the capability fields All three wrote fields the site no longer reads. The slash bot and the AI bot accepted has_arm=true and github=owner/repo; push-json mapped has_arm into the payload. After the schema change those writes would have gone to columns that are not there. architectures and platforms are multi-selects over a closed set, so both bots reject an unknown value instead of storing it — the same guarantee the field type gives in the admin UI. push-json still reads has_arm when architectures is absent, because ProxmoxVED's json files carry the old key until they are converted. * Drop the docker change again It came back through the merge in 35735dae: the branch was pushed before the revert, so merging the remote copy restored it. Removing it forward rather than rewriting history that is already published. --- .github/workflows/pocketbase-ai-bot.yml | 20 +++++++- .github/workflows/pocketbase-bot.yml | 31 +++++++++--- .github/workflows/push-json-to-pocketbase.yml | 14 +++++- CONTRIBUTING.md | 49 +++++++++++++++++++ ct/forgejo-runner.sh | 5 ++ ct/pangolin.sh | 3 ++ install/pangolin-install.sh | 13 ++++- 7 files changed, 124 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pocketbase-ai-bot.yml b/.github/workflows/pocketbase-ai-bot.yml index e3840ea64..5ee005c6f 100644 --- a/.github/workflows/pocketbase-ai-bot.yml +++ b/.github/workflows/pocketbase-ai-bot.yml @@ -314,12 +314,17 @@ jobs: // ── Allow-lists (mirror the slash bot) ───────────────────────────── const ALLOWED_FIELDS = { name: 'string', description: 'string', logo: 'string', documentation: 'string', - website: 'string', project_url: 'string', github: 'string', config_path: 'string', + website: 'string', project_url: 'string', repository: 'string', config_path: 'string', tags: 'string', port: 'number', default_user: 'nullable_string', default_passwd: 'nullable_string', - unprivileged: 'number', updateable: 'boolean', privileged: 'boolean', has_arm: 'boolean', + unprivileged: 'number', updateable: 'boolean', privileged: 'boolean', + architectures: 'select_list', platforms: 'select_list', is_dev: 'boolean', is_disabled: 'boolean', disable_message: 'string', is_deleted: 'boolean', deleted_message: 'string' }; + const SELECT_VALUES = { + architectures: ['amd64', 'arm64'], + platforms: ['pve', 'incus'], + }; const FIELD_TO_CT_VAR = { tags: 'var_tags', unprivileged: 'var_unprivileged' }; const RESOURCE_KEYS = { cpu: 'number', ram: 'number', hdd: 'number', os: 'string', version: 'string' }; const METHOD_KEYS = { config_path: 'string', script: 'string' }; @@ -339,6 +344,17 @@ jobs: if (isNaN(n)) return { error: '`' + key + '` must be a number' }; return { value: n }; } + if (type === 'select_list') { + // architectures/platforms are multi-selects over a closed set, + // so an unknown value is rejected rather than stored. The model + // may hand back an array or a comma-separated string. + const allowed = SELECT_VALUES[key] || []; + const raw = Array.isArray(rawVal) ? rawVal : String(rawVal == null ? '' : rawVal).split(','); + const values = raw.map(function (v) { return String(v).trim(); }).filter(Boolean); + const bad = values.filter(function (v) { return allowed.indexOf(v) === -1; }); + if (bad.length > 0) return { error: '`' + key + '` accepts ' + allowed.join(', ') + ' — got: ' + bad.join(', ') }; + return { value: values }; + } if (type === 'nullable_string') return { value: rawVal === '' || rawVal == null ? null : String(rawVal) }; return { value: String(rawVal) }; } diff --git a/.github/workflows/pocketbase-bot.yml b/.github/workflows/pocketbase-bot.yml index 9d824348d..b22b0755e 100644 --- a/.github/workflows/pocketbase-bot.yml +++ b/.github/workflows/pocketbase-bot.yml @@ -323,9 +323,10 @@ jobs: '/pocketbase method remove \n' + '```\n' + 'Method fields: `cpu` `ram` `hdd` `os` `version` `config_path` `script`\n\n' + - '**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' + + '**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `repository` ' + '`config_path` `port` `default_user` `default_passwd` ' + - '`updateable` `privileged` `has_arm` `is_dev` ' + + '`updateable` `privileged` `is_dev` ' + + '`architectures` (amd64,arm64) `platforms` (pve,incus) ' + '`is_disabled` `disable_message` `is_deleted` `deleted_message`'; if (!withoutCmd) { @@ -504,7 +505,8 @@ jobs: out.push('- **Port:** ' + (record.port != null ? '`' + record.port + '`' : '—')); out.push('- **Updateable:** ' + (record.updateable ? 'Yes' : 'No')); out.push('- **Privileged:** ' + (record.privileged ? 'Yes' : 'No')); - out.push('- **ARM:** ' + (record.has_arm ? 'Yes' : 'No')); + out.push('- **Architectures:** ' + ((record.architectures || []).join(', ') || 'amd64')); + out.push('- **Platforms:** ' + ((record.platforms || []).join(', ') || 'pve')); if (record.is_dev) out.push('- **Dev:** Yes'); if (record.is_disabled) out.push('- **Disabled:** Yes' + (record.disable_message ? ' — ' + record.disable_message : '')); if (record.is_deleted) out.push('- **Deleted:** Yes' + (record.deleted_message ? ' — ' + record.deleted_message : '')); @@ -838,7 +840,7 @@ jobs: const fieldName = setMatch[1].toLowerCase(); const SET_ALLOWED = { name: 'string', description: 'string', logo: 'string', - documentation: 'string', website: 'string', project_url: 'string', github: 'string', + documentation: 'string', website: 'string', project_url: 'string', repository: 'string', config_path: 'string', disable_message: 'string', deleted_message: 'string' }; if (!SET_ALLOWED[fieldName]) { @@ -888,7 +890,7 @@ jobs: documentation: 'string', website: 'string', project_url: 'string', - github: 'string', + repository: 'string', config_path: 'string', tags: 'string', port: 'number', @@ -897,7 +899,8 @@ jobs: unprivileged: 'number', updateable: 'boolean', privileged: 'boolean', - has_arm: 'boolean', + architectures: 'select_list', + platforms: 'select_list', is_dev: 'boolean', is_disabled: 'boolean', disable_message: 'string', @@ -924,6 +927,10 @@ jobs: } // Cast values to correct types + const SELECT_VALUES = { + architectures: ['amd64', 'arm64'], + platforms: ['pve', 'incus'], + }; const payload = {}; for (const [key, rawVal] of Object.entries(parsedFields)) { const type = ALLOWED_FIELDS[key]; @@ -943,6 +950,18 @@ jobs: process.exit(0); } payload[key] = n; + } else if (type === 'select_list') { + // architectures/platforms are multi-selects over a closed set, + // so an unknown value is rejected rather than stored. + const allowed = SELECT_VALUES[key] || []; + const values = rawVal.split(',').map(function (v) { return v.trim(); }).filter(Boolean); + const bad = values.filter(function (v) { return allowed.indexOf(v) === -1; }); + if (bad.length > 0) { + await addReaction('-1'); + await postComment('❌ **PocketBase Bot**: `' + key + '` accepts ' + allowed.join(', ') + ' — got: `' + bad.join(', ') + '`'); + process.exit(0); + } + payload[key] = values; } else if (type === 'nullable_string') { payload[key] = rawVal === '' ? null : rawVal; } else { diff --git a/.github/workflows/push-json-to-pocketbase.yml b/.github/workflows/push-json-to-pocketbase.yml index 9a58adf90..23d0f4f2e 100644 --- a/.github/workflows/push-json-to-pocketbase.yml +++ b/.github/workflows/push-json-to-pocketbase.yml @@ -179,7 +179,19 @@ jobs: if (resolvedType) payload.type = resolvedType; var resolvedCats = (data.categories || []).map(function(n) { return categoryNameToPbId[categoryIdToName[n]]; }).filter(Boolean); if (resolvedCats.length) payload.categories = resolvedCats; - if (data.has_arm !== undefined) payload.has_arm = data.has_arm === true || data.has_arm === 'true'; + // architectures replaces has_arm: a boolean could say "also ARM" but + // not "ARM only" or "amd64 only". A record written before the field + // existed is read through the old key so nothing silently drops. + if (Array.isArray(data.architectures)) { + payload.architectures = data.architectures.filter(function (a) { return a === 'amd64' || a === 'arm64'; }); + } else if (data.has_arm !== undefined) { + payload.architectures = (data.has_arm === true || data.has_arm === 'true') ? ['amd64', 'arm64'] : ['amd64']; + } + if (Array.isArray(data.platforms)) { + payload.platforms = data.platforms.filter(function (p) { return p === 'pve' || p === 'incus'; }); + } + if (Array.isArray(data.app_vars)) payload.app_vars = data.app_vars; + if (data.repository !== undefined) payload.repository = data.repository; if (data.version !== undefined) payload.version = data.version; if (data.changelog !== undefined) payload.changelog = data.changelog; if (data.screenshots !== undefined) payload.screenshots = data.screenshots; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6493ff4f6..a84ae9cc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -103,6 +103,55 @@ Key rules at a glance: - Quote all variables: `"$VAR"` not `$VAR` - Use lowercase variable names - Do not hardcode credentials or sensitive values +- Never prompt without an escape hatch — see below + +### Answering a prompt up front + +An install script that only asks cannot be deployed unattended. Read the +variable first and prompt only when it is unset: + +```bash +if [[ -z "${var_admin_user:-}" ]]; then + read -r -p "${TAB3}Admin username: " var_admin_user +fi +var_admin_user="${var_admin_user:-admin}" +``` + +Name them `var_`, the same namespace the container variables use. +`install/forgejo-runner-install.sh`, `install/pangolin-install.sh`, and `install/docker-install.sh` all +follow this. + +The variable also has to be exported from `ct/.sh`, or it never reaches +the container — `lxc-attach` carries the caller's environment, but only for +what was actually exported: + +```bash +export var_admin_user="${var_admin_user:-}" +``` + +Declare them on the script's PocketBase record in `app_vars` so the website's +generator can offer them as fields: + +```json +"app_vars": [ + { + "name": "var_admin_user", + "label": "Admin Username", + "type": "text", + "default": "admin" + }, + { + "name": "var_admin_pass", + "label": "Admin Password", + "type": "password", + "secret": true + } +] +``` + +`type` is one of `text`, `password`, `number`, `boolean` (`yes`/`no`) or +`select` (with `options`). Mark anything credential-like `secret` — the +generator keeps those out of shareable links and out of the on-screen summary. Full standards and examples: **[community-scripts.org/docs/contribution](https://community-scripts.org/docs/contribution)** diff --git a/ct/forgejo-runner.sh b/ct/forgejo-runner.sh index 7c7c1774f..1184c22db 100644 --- a/ct/forgejo-runner.sh +++ b/ct/forgejo-runner.sh @@ -18,6 +18,7 @@ var_nesting="${var_nesting:-1}" var_keyctl="${var_keyctl:-1}" export var_forgejo_instance="${var_forgejo_instance:-}" +export var_forgejo_runner_uuid="${var_forgejo_runner_uuid:-}" export var_forgejo_runner_token="${var_forgejo_runner_token:-}" export var_runner_labels="${var_runner_labels:-}" @@ -64,6 +65,10 @@ if [[ -n "${mode:-}" ]]; then msg_error "var_forgejo_instance is required for unattended installs." exit 1 fi + if [[ -z "${var_forgejo_runner_uuid:-}" ]]; then + msg_error "var_forgejo_runner_uuid is required for unattended installs." + exit 1 + fi if [[ -z "${var_forgejo_runner_token:-}" ]]; then msg_error "var_forgejo_runner_token is required for unattended installs." exit 1 diff --git a/ct/pangolin.sh b/ct/pangolin.sh index 542c9c510..b69584e52 100644 --- a/ct/pangolin.sh +++ b/ct/pangolin.sh @@ -17,6 +17,9 @@ var_arm64="${var_arm64:-yes}" var_unprivileged="${var_unprivileged:-1}" var_tun="${var_tun:-1}" +export var_pangolin_url="${var_pangolin_url:-}" +export var_pangolin_email="${var_pangolin_email:-}" + header_info "$APP" variables color diff --git a/install/pangolin-install.sh b/install/pangolin-install.sh index 50b662851..ae8582f2a 100644 --- a/install/pangolin-install.sh +++ b/install/pangolin-install.sh @@ -27,9 +27,18 @@ fetch_and_deploy_gh_release "pangolin" "fosrl/pangolin" "tarball" "$PANGOLIN_VER fetch_and_deploy_gh_release "gerbil" "fosrl/gerbil" "singlefile" "latest" "/usr/bin" "gerbil_linux_$(arch_resolve)" fetch_and_deploy_gh_release "traefik" "traefik/traefik" "prebuild" "latest" "/usr/bin" "traefik_v*_linux_$(arch_resolve).tar.gz" -read -rp "${TAB3}Enter your Pangolin URL (ex: https://pangolin.example.com): " pango_url +# Read the variable first and prompt only when it is unset, so the install can +# be supplied up front. Same convention as install/forgejo-runner-install.sh. +pango_url="${var_pangolin_url:-}" +if [[ -z "$pango_url" ]]; then + read -rp "${TAB3}Enter your Pangolin URL (ex: https://pangolin.example.com): " pango_url +fi [[ "$pango_url" != https://* && "$pango_url" != http://* ]] && pango_url="https://${pango_url}" -read -rp "${TAB3}Enter your email address: " pango_email + +pango_email="${var_pangolin_email:-}" +if [[ -z "$pango_email" ]]; then + read -rp "${TAB3}Enter your email address: " pango_email +fi msg_info "Setup Pangolin" SECRET_KEY=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 32)