From ff8fdc853ee3f05018608163ec0fcd0ea82152d3 Mon Sep 17 00:00:00 2001 From: MickLesk Date: Thu, 6 Aug 2026 17:15:10 +0200 Subject: [PATCH] Teach the PocketBase workflows the capability fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/pocketbase-ai-bot.yml | 20 ++++++++++-- .github/workflows/pocketbase-bot.yml | 31 +++++++++++++++---- .github/workflows/push-json-to-pocketbase.yml | 14 ++++++++- 3 files changed, 56 insertions(+), 9 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;