mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-04-14 14:32:16 +00:00
Compare commits
9 Commits
copilot/fi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9c2307b0e | ||
|
|
0e809c6ee9 | ||
|
|
64d000b73a | ||
|
|
65353d01e1 | ||
|
|
683be87e9e | ||
|
|
5894734857 | ||
|
|
c881811499 | ||
|
|
33716c92e5 | ||
|
|
dda2ea811f |
474
.github/workflows/pocketbase-bot.yml
generated
vendored
474
.github/workflows/pocketbase-bot.yml
generated
vendored
@@ -31,6 +31,8 @@ jobs:
|
||||
ACTOR: ${{ github.event.comment.user.login }}
|
||||
ACTOR_ASSOCIATION: ${{ github.event.comment.author_association }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
|
||||
REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
@@ -113,7 +115,6 @@ jobs:
|
||||
}
|
||||
|
||||
// ── Permission check ───────────────────────────────────────────────
|
||||
// author_association: OWNER = repo/org owner, MEMBER = org member (includes Contributors team)
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
@@ -128,18 +129,11 @@ jobs:
|
||||
await addReaction('eyes');
|
||||
|
||||
// ── Parse command ──────────────────────────────────────────────────
|
||||
// Formats (first line of comment):
|
||||
// /pocketbase <slug> field=value [field=value ...] ← field updates (simple values)
|
||||
// /pocketbase <slug> set <field> ← value from code block below
|
||||
// /pocketbase <slug> note list|add|edit|remove ... ← note management
|
||||
// /pocketbase <slug> method list ← list install methods
|
||||
// /pocketbase <slug> method <type> cpu=N ram=N hdd=N ← edit install method resources
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
const lines = commentBody.trim().split('\n');
|
||||
const firstLine = lines[0].trim();
|
||||
const withoutCmd = firstLine.replace(/^\/pocketbase\s+/, '').trim();
|
||||
|
||||
// Extract code block content from comment body (```...``` or ```lang\n...```)
|
||||
function extractCodeBlock(body) {
|
||||
const m = body.match(/```[^\n]*\n([\s\S]*?)```/);
|
||||
return m ? m[1].trim() : null;
|
||||
@@ -147,6 +141,8 @@ jobs:
|
||||
const codeBlockValue = extractCodeBlock(commentBody);
|
||||
|
||||
const HELP_TEXT =
|
||||
'**Show current state:**\n' +
|
||||
'```\n/pocketbase <slug> info\n```\n\n' +
|
||||
'**Field update (simple):** `/pocketbase <slug> field=value [field=value ...]`\n\n' +
|
||||
'**Field update (HTML/multiline) — value from code block:**\n' +
|
||||
'````\n' +
|
||||
@@ -162,12 +158,16 @@ jobs:
|
||||
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
|
||||
'/pocketbase <slug> note remove <type> "<text>"\n' +
|
||||
'```\n\n' +
|
||||
'**Install method resources:**\n' +
|
||||
'**Install method management:**\n' +
|
||||
'```\n' +
|
||||
'/pocketbase <slug> method list\n' +
|
||||
'/pocketbase <slug> method <type> hdd=10\n' +
|
||||
'/pocketbase <slug> method <type> cpu=4 ram=2048 hdd=20\n' +
|
||||
'```\n\n' +
|
||||
'/pocketbase <slug> method <type> config_path="/opt/app/.env"\n' +
|
||||
'/pocketbase <slug> method <type> os=debian version=13\n' +
|
||||
'/pocketbase <slug> method add <type> cpu=2 ram=2048 hdd=8 os=debian version=13\n' +
|
||||
'/pocketbase <slug> method remove <type>\n' +
|
||||
'```\n' +
|
||||
'Method fields: `cpu` `ram` `hdd` `os` `version` `config_path` `script`\n\n' +
|
||||
'**Editable fields:** `name` `description` `logo` `documentation` `website` `project_url` `github` ' +
|
||||
'`config_path` `port` `default_user` `default_passwd` ' +
|
||||
'`updateable` `privileged` `has_arm` `is_dev` ' +
|
||||
@@ -189,8 +189,7 @@ jobs:
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Allowed fields and their types ─────────────────────────────────
|
||||
// ── PocketBase: authenticate (shared by all paths) ─────────────────
|
||||
// ── PocketBase: authenticate ───────────────────────────────────────
|
||||
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
|
||||
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
|
||||
const coll = process.env.POCKETBASE_COLLECTION;
|
||||
@@ -210,7 +209,7 @@ jobs:
|
||||
}
|
||||
const token = JSON.parse(authRes.body).token;
|
||||
|
||||
// ── PocketBase: find record by slug (shared by all paths) ──────────
|
||||
// ── PocketBase: find record by slug ────────────────────────────────
|
||||
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
|
||||
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
|
||||
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
|
||||
@@ -228,57 +227,164 @@ jobs:
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── Shared helpers ─────────────────────────────────────────────────
|
||||
|
||||
// Key=value parser: handles unquoted and "quoted" values
|
||||
function parseKVPairs(str) {
|
||||
const fields = {};
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
let keyStart = pos;
|
||||
while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++;
|
||||
const key = str.substring(keyStart, pos).trim();
|
||||
if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; }
|
||||
pos++;
|
||||
let value;
|
||||
if (pos < str.length && str[pos] === '"') {
|
||||
pos++;
|
||||
let valStart = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
value = str.substring(valStart, pos).replace(/\\"/g, '"');
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let valStart = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
value = str.substring(valStart, pos);
|
||||
}
|
||||
fields[key] = value;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Token parser for note commands: unquoted-word OR "quoted string"
|
||||
function parseTokens(str) {
|
||||
const tokens = [];
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
if (str[pos] === '"') {
|
||||
pos++;
|
||||
let start = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
tokens.push(str.substring(start, pos).replace(/\\"/g, '"'));
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let start = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
tokens.push(str.substring(start, pos));
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Read JSON blob from record (handles parsed objects and strings)
|
||||
function readJsonBlob(val) {
|
||||
if (Array.isArray(val)) return val;
|
||||
try { return JSON.parse(val || '[]'); } catch (e) { return []; }
|
||||
}
|
||||
|
||||
// Frontend cache revalidation (silent, best-effort)
|
||||
async function revalidate(s) {
|
||||
const frontendUrl = process.env.FRONTEND_URL;
|
||||
const secret = process.env.REVALIDATE_SECRET;
|
||||
if (!frontendUrl || !secret) return;
|
||||
try {
|
||||
await request(frontendUrl.replace(/\/$/, '') + '/api/revalidate', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + secret, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tags: ['scripts', 'script-' + s] })
|
||||
});
|
||||
} catch (e) { console.warn('Revalidation skipped:', e.message); }
|
||||
}
|
||||
|
||||
// Format notes list for display
|
||||
function formatNotesList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (n, i) {
|
||||
return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || '');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// Format install methods list for display
|
||||
function formatMethodsList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (im, i) {
|
||||
const r = im.resources || {};
|
||||
const parts = [
|
||||
(r.os || '?') + ' ' + (r.version || '?'),
|
||||
(r.cpu != null ? r.cpu : '?') + 'C / ' + (r.ram != null ? r.ram : '?') + ' MB / ' + (r.hdd != null ? r.hdd : '?') + ' GB'
|
||||
];
|
||||
if (im.config_path) parts.push('config: `' + im.config_path + '`');
|
||||
if (im.script) parts.push('script: `' + im.script + '`');
|
||||
return (i + 1) + '. **`' + (im.type || '?') + '`** — ' + parts.join(', ');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
// ── Route: dispatch to subcommand handler ──────────────────────────
|
||||
const infoMatch = rest.match(/^info$/i);
|
||||
const noteMatch = rest.match(/^note\s+(list|add|edit|remove)\b/i);
|
||||
const methodMatch = rest.match(/^method\b/i);
|
||||
const setMatch = rest.match(/^set\s+(\S+)/i);
|
||||
|
||||
if (noteMatch) {
|
||||
// ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ────
|
||||
if (infoMatch) {
|
||||
// ── INFO SUBCOMMAND ──────────────────────────────────────────────
|
||||
const notesArr = readJsonBlob(record.notes_json);
|
||||
const methodsArr = readJsonBlob(record.install_methods_json);
|
||||
|
||||
const out = [];
|
||||
out.push('ℹ️ **PocketBase Bot**: Info for **`' + slug + '`**\n');
|
||||
|
||||
out.push('**Basic info:**');
|
||||
out.push('- **Name:** ' + (record.name || '—'));
|
||||
out.push('- **Slug:** `' + slug + '`');
|
||||
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'));
|
||||
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 : ''));
|
||||
out.push('');
|
||||
|
||||
out.push('**Links:**');
|
||||
out.push('- **Website:** ' + (record.website || '—'));
|
||||
out.push('- **Docs:** ' + (record.documentation || '—'));
|
||||
out.push('- **Logo:** ' + (record.logo ? '[link](' + record.logo + ')' : '—'));
|
||||
out.push('- **GitHub:** ' + (record.github || '—'));
|
||||
if (record.config_path) out.push('- **Config:** `' + record.config_path + '`');
|
||||
out.push('');
|
||||
|
||||
out.push('**Credentials:**');
|
||||
out.push('- **User:** ' + (record.default_user || '—'));
|
||||
out.push('- **Password:** ' + (record.default_passwd ? '*(set)*' : '—'));
|
||||
out.push('');
|
||||
|
||||
out.push('**Install methods** (' + methodsArr.length + '):');
|
||||
out.push(formatMethodsList(methodsArr));
|
||||
out.push('');
|
||||
|
||||
out.push('**Notes** (' + notesArr.length + '):');
|
||||
out.push(formatNotesList(notesArr));
|
||||
|
||||
await addReaction('+1');
|
||||
await postComment(out.join('\n'));
|
||||
|
||||
} else if (noteMatch) {
|
||||
// ── NOTE SUBCOMMAND ──────────────────────────────────────────────
|
||||
const noteAction = noteMatch[1].toLowerCase();
|
||||
const noteArgsStr = rest.substring(noteMatch[0].length).trim();
|
||||
let notesArr = readJsonBlob(record.notes_json);
|
||||
|
||||
// Parse notes_json from the already-fetched script record
|
||||
// PocketBase may return JSON fields as already-parsed objects
|
||||
let notesArr = [];
|
||||
try {
|
||||
const rawNotes = record.notes_json;
|
||||
notesArr = Array.isArray(rawNotes) ? rawNotes : JSON.parse(rawNotes || '[]');
|
||||
} catch (e) { notesArr = []; }
|
||||
|
||||
// Token parser: unquoted-word OR "quoted string" (supports \" escapes)
|
||||
function parseNoteTokens(str) {
|
||||
const tokens = [];
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
if (str[pos] === '"') {
|
||||
pos++;
|
||||
let start = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
tokens.push(str.substring(start, pos).replace(/\\"/g, '"'));
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let start = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
tokens.push(str.substring(start, pos));
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function formatNotesList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (n, i) {
|
||||
return (i + 1) + '. **`' + (n.type || '?') + '`**: ' + (n.text || '');
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
async function patchNotesJson(arr) {
|
||||
async function patchNotes(arr) {
|
||||
const res = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
@@ -286,7 +392,7 @@ jobs:
|
||||
});
|
||||
if (!res.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update notes:\n```\n' + res.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -299,7 +405,7 @@ jobs:
|
||||
);
|
||||
|
||||
} else if (noteAction === 'add') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
const tokens = parseTokens(noteArgsStr);
|
||||
if (tokens.length < 2) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
@@ -311,7 +417,8 @@ jobs:
|
||||
const noteType = tokens[0].toLowerCase();
|
||||
const noteText = tokens.slice(1).join(' ');
|
||||
notesArr.push({ type: noteType, text: noteText });
|
||||
await patchNotesJson(notesArr);
|
||||
await patchNotes(notesArr);
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' +
|
||||
@@ -321,7 +428,7 @@ jobs:
|
||||
);
|
||||
|
||||
} else if (noteAction === 'edit') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
const tokens = parseTokens(noteArgsStr);
|
||||
if (tokens.length < 3) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
@@ -346,7 +453,8 @@ jobs:
|
||||
process.exit(0);
|
||||
}
|
||||
notesArr[idx].text = newText;
|
||||
await patchNotesJson(notesArr);
|
||||
await patchNotes(notesArr);
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' +
|
||||
@@ -357,7 +465,7 @@ jobs:
|
||||
);
|
||||
|
||||
} else if (noteAction === 'remove') {
|
||||
const tokens = parseNoteTokens(noteArgsStr);
|
||||
const tokens = parseTokens(noteArgsStr);
|
||||
if (tokens.length < 2) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
@@ -381,7 +489,8 @@ jobs:
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
await patchNotesJson(notesArr);
|
||||
await patchNotes(notesArr);
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' +
|
||||
@@ -392,36 +501,36 @@ jobs:
|
||||
}
|
||||
|
||||
} else if (methodMatch) {
|
||||
// ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ──
|
||||
// ── METHOD SUBCOMMAND ────────────────────────────────────────────
|
||||
const methodArgs = rest.replace(/^method\s*/i, '').trim();
|
||||
const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list';
|
||||
let methodsArr = readJsonBlob(record.install_methods_json);
|
||||
|
||||
// Parse install_methods_json from the already-fetched script record
|
||||
// PocketBase may return JSON fields as already-parsed objects
|
||||
let methodsArr = [];
|
||||
try {
|
||||
const rawMethods = record.install_methods_json;
|
||||
methodsArr = Array.isArray(rawMethods) ? rawMethods : JSON.parse(rawMethods || '[]');
|
||||
} catch (e) { methodsArr = []; }
|
||||
// Method field classification
|
||||
const RESOURCE_KEYS = { cpu: 'number', ram: 'number', hdd: 'number', os: 'string', version: 'string' };
|
||||
const METHOD_KEYS = { config_path: 'string', script: 'string' };
|
||||
const ALL_METHOD_KEYS = Object.assign({}, RESOURCE_KEYS, METHOD_KEYS);
|
||||
|
||||
function formatMethodsList(arr) {
|
||||
if (arr.length === 0) return '*None*';
|
||||
return arr.map(function (im, i) {
|
||||
const r = im.resources || {};
|
||||
return (i + 1) + '. **`' + (im.type || '?') + '`** — CPU: `' + (r.cpu != null ? r.cpu : '?') +
|
||||
'` · RAM: `' + (r.ram != null ? r.ram : '?') + ' MB` · HDD: `' + (r.hdd != null ? r.hdd : '?') + ' GB`';
|
||||
}).join('\n');
|
||||
function applyMethodChanges(method, parsed) {
|
||||
if (!method.resources) method.resources = {};
|
||||
for (const [k, v] of Object.entries(parsed)) {
|
||||
if (RESOURCE_KEYS[k]) {
|
||||
method.resources[k] = RESOURCE_KEYS[k] === 'number' ? parseInt(v, 10) : v;
|
||||
} else if (METHOD_KEYS[k]) {
|
||||
method[k] = v === '' ? null : v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function patchInstallMethodsJson(arr) {
|
||||
async function patchMethods(arr) {
|
||||
const res = await request(recordsUrl + '/' + record.id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ install_methods_json: JSON.stringify(arr) })
|
||||
body: JSON.stringify({ install_methods_json: arr })
|
||||
});
|
||||
if (!res.ok) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```');
|
||||
await postComment('❌ **PocketBase Bot**: Failed to update install methods:\n```\n' + res.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -432,70 +541,122 @@ jobs:
|
||||
'ℹ️ **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' +
|
||||
formatMethodsList(methodsArr)
|
||||
);
|
||||
|
||||
} else {
|
||||
// Parse: <type> cpu=N ram=N hdd=N
|
||||
const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/);
|
||||
if (!methodParts) {
|
||||
await addReaction('-1');
|
||||
// Check for add / remove sub-actions
|
||||
const addMatch = methodArgs.match(/^add\s+(\S+)(?:\s+(.+))?$/i);
|
||||
const removeMatch = methodArgs.match(/^remove\s+(\S+)$/i);
|
||||
|
||||
if (addMatch) {
|
||||
// ── METHOD ADD ───────────────────────────────────────────────
|
||||
const newType = addMatch[1];
|
||||
if (methodsArr.some(function (im) { return (im.type || '').toLowerCase() === newType.toLowerCase(); })) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Install method `' + newType + '` already exists for `' + slug + '`.\n\nUse `/pocketbase ' + slug + ' method list` to see all methods.');
|
||||
process.exit(0);
|
||||
}
|
||||
const newMethod = { type: newType, resources: { cpu: 1, ram: 512, hdd: 4, os: 'debian', version: '13' } };
|
||||
if (addMatch[2]) {
|
||||
const parsed = parseKVPairs(addMatch[2]);
|
||||
const unknown = Object.keys(parsed).filter(function (k) { return !ALL_METHOD_KEYS[k]; });
|
||||
if (unknown.length > 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Unknown method field(s): `' + unknown.join('`, `') + '`\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
applyMethodChanges(newMethod, parsed);
|
||||
}
|
||||
methodsArr.push(newMethod);
|
||||
await patchMethods(methodsArr);
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' +
|
||||
'**Usage:**\n```\n/pocketbase ' + slug + ' method list\n/pocketbase ' + slug + ' method <type> hdd=10\n/pocketbase ' + slug + ' method <type> cpu=4 ram=2048 hdd=20\n```'
|
||||
'✅ **PocketBase Bot**: Added install method **`' + newType + '`** to **`' + slug + '`**\n\n' +
|
||||
formatMethodsList([newMethod]) + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const targetType = methodParts[1].toLowerCase();
|
||||
const resourcesStr = methodParts[2];
|
||||
|
||||
// Parse resource fields (only cpu/ram/hdd allowed)
|
||||
const RESOURCE_FIELDS = { cpu: true, ram: true, hdd: true };
|
||||
const resourceChanges = {};
|
||||
const rePairs = /([a-z]+)=(\d+)/gi;
|
||||
let m;
|
||||
while ((m = rePairs.exec(resourcesStr)) !== null) {
|
||||
const key = m[1].toLowerCase();
|
||||
if (RESOURCE_FIELDS[key]) resourceChanges[key] = parseInt(m[2], 10);
|
||||
}
|
||||
if (Object.keys(resourceChanges).length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: No valid resource fields found. Use `cpu=N`, `ram=N`, `hdd=N`.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find matching method by type name (case-insensitive)
|
||||
const idx = methodsArr.findIndex(function (im) {
|
||||
return (im.type || '').toLowerCase() === targetType;
|
||||
});
|
||||
if (idx === -1) {
|
||||
await addReaction('-1');
|
||||
const availableTypes = methodsArr.map(function (im) { return im.type || '?'; });
|
||||
} else if (removeMatch) {
|
||||
// ── METHOD REMOVE ────────────────────────────────────────────
|
||||
const removeType = removeMatch[1].toLowerCase();
|
||||
const removed = methodsArr.filter(function (im) { return (im.type || '').toLowerCase() === removeType; });
|
||||
if (removed.length === 0) {
|
||||
await addReaction('-1');
|
||||
const available = methodsArr.map(function (im) { return im.type || '?'; });
|
||||
await postComment('❌ **PocketBase Bot**: No install method `' + removeType + '` found.\n\n**Available:** `' + (available.length ? available.join('`, `') : '(none)') + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
methodsArr = methodsArr.filter(function (im) { return (im.type || '').toLowerCase() !== removeType; });
|
||||
await patchMethods(methodsArr);
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No install method with type `' + targetType + '` found for `' + slug + '`.\n\n' +
|
||||
'**Available types:** `' + (availableTypes.length ? availableTypes.join('`, `') : '(none)') + '`\n\n' +
|
||||
'Use `/pocketbase ' + slug + ' method list` to see all methods.'
|
||||
'✅ **PocketBase Bot**: Removed install method **`' + removed[0].type + '`** from **`' + slug + '`**\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
|
||||
} else {
|
||||
// ── METHOD EDIT ──────────────────────────────────────────────
|
||||
const editParts = methodArgs.match(/^(\S+)\s+(.+)$/);
|
||||
if (!editParts) {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: Invalid `method` syntax.\n\n' +
|
||||
'**Usage:**\n```\n/pocketbase ' + slug + ' method list\n' +
|
||||
'/pocketbase ' + slug + ' method <type> cpu=4 ram=2048 hdd=20\n' +
|
||||
'/pocketbase ' + slug + ' method <type> config_path="/opt/app/.env"\n' +
|
||||
'/pocketbase ' + slug + ' method add <type> cpu=2 ram=2048 hdd=8\n' +
|
||||
'/pocketbase ' + slug + ' method remove <type>\n```'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
const targetType = editParts[1].toLowerCase();
|
||||
const parsed = parseKVPairs(editParts[2]);
|
||||
|
||||
const unknown = Object.keys(parsed).filter(function (k) { return !ALL_METHOD_KEYS[k]; });
|
||||
if (unknown.length > 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: Unknown method field(s): `' + unknown.join('`, `') + '`\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
if (Object.keys(parsed).length === 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: No valid `key=value` pairs found.\n\n**Allowed:** `' + Object.keys(ALL_METHOD_KEYS).join('`, `') + '`');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const idx = methodsArr.findIndex(function (im) { return (im.type || '').toLowerCase() === targetType; });
|
||||
if (idx === -1) {
|
||||
await addReaction('-1');
|
||||
const available = methodsArr.map(function (im) { return im.type || '?'; });
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: No install method `' + targetType + '` found for `' + slug + '`.\n\n' +
|
||||
'**Available:** `' + (available.length ? available.join('`, `') : '(none)') + '`\n\n' +
|
||||
'Use `/pocketbase ' + slug + ' method list` to see all methods.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
applyMethodChanges(methodsArr[idx], parsed);
|
||||
await patchMethods(methodsArr);
|
||||
await revalidate(slug);
|
||||
|
||||
const changesLines = Object.entries(parsed)
|
||||
.map(function ([k, v]) {
|
||||
const unit = k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '';
|
||||
return '- `' + k + '` → `' + v + unit + '`';
|
||||
}).join('\n');
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' +
|
||||
'**Changes applied:**\n' + changesLines + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!methodsArr[idx].resources) methodsArr[idx].resources = {};
|
||||
if (resourceChanges.cpu != null) methodsArr[idx].resources.cpu = resourceChanges.cpu;
|
||||
if (resourceChanges.ram != null) methodsArr[idx].resources.ram = resourceChanges.ram;
|
||||
if (resourceChanges.hdd != null) methodsArr[idx].resources.hdd = resourceChanges.hdd;
|
||||
|
||||
await patchInstallMethodsJson(methodsArr);
|
||||
|
||||
const changesLines = Object.entries(resourceChanges)
|
||||
.map(function ([k, v]) { return '- `' + k + '` → `' + v + (k === 'ram' ? ' MB' : k === 'hdd' ? ' GB' : '') + '`'; })
|
||||
.join('\n');
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
'✅ **PocketBase Bot**: Updated install method **`' + methodsArr[idx].type + '`** for **`' + slug + '`**\n\n' +
|
||||
'**Changes applied:**\n' + changesLines + '\n\n' +
|
||||
'*Executed by @' + actor + '*'
|
||||
);
|
||||
}
|
||||
|
||||
} else if (setMatch) {
|
||||
// ── SET SUBCOMMAND (multi-line / HTML / special chars via code block) ──
|
||||
// ── SET SUBCOMMAND (value from code block) ───────────────────────
|
||||
const fieldName = setMatch[1].toLowerCase();
|
||||
const SET_ALLOWED = {
|
||||
name: 'string', description: 'string', logo: 'string',
|
||||
@@ -531,6 +692,7 @@ jobs:
|
||||
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + setPatchRes.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
await revalidate(slug);
|
||||
const preview = codeBlockValue.length > 300 ? codeBlockValue.substring(0, 300) + '…' : codeBlockValue;
|
||||
await addReaction('+1');
|
||||
await postComment(
|
||||
@@ -541,11 +703,6 @@ jobs:
|
||||
|
||||
} else {
|
||||
// ── FIELD=VALUE PATH ─────────────────────────────────────────────
|
||||
const fieldsStr = rest;
|
||||
|
||||
// Skipped: slug, script_created/updated, created (auto), categories/
|
||||
// install_methods/notes/type (relations), github_data/install_methods_json/
|
||||
// notes_json (auto-generated), execute_in (select relation), last_update_commit (auto)
|
||||
const ALLOWED_FIELDS = {
|
||||
name: 'string',
|
||||
description: 'string',
|
||||
@@ -568,39 +725,7 @@ jobs:
|
||||
deleted_message: 'string',
|
||||
};
|
||||
|
||||
// Field=value parser (handles quoted values and empty=null)
|
||||
function parseFields(str) {
|
||||
const fields = {};
|
||||
let pos = 0;
|
||||
while (pos < str.length) {
|
||||
while (pos < str.length && /\s/.test(str[pos])) pos++;
|
||||
if (pos >= str.length) break;
|
||||
let keyStart = pos;
|
||||
while (pos < str.length && str[pos] !== '=' && !/\s/.test(str[pos])) pos++;
|
||||
const key = str.substring(keyStart, pos).trim();
|
||||
if (!key || pos >= str.length || str[pos] !== '=') { pos++; continue; }
|
||||
pos++;
|
||||
let value;
|
||||
if (str[pos] === '"') {
|
||||
pos++;
|
||||
let valStart = pos;
|
||||
while (pos < str.length && str[pos] !== '"') {
|
||||
if (str[pos] === '\\') pos++;
|
||||
pos++;
|
||||
}
|
||||
value = str.substring(valStart, pos).replace(/\\"/g, '"');
|
||||
if (pos < str.length) pos++;
|
||||
} else {
|
||||
let valStart = pos;
|
||||
while (pos < str.length && !/\s/.test(str[pos])) pos++;
|
||||
value = str.substring(valStart, pos);
|
||||
}
|
||||
fields[key] = value;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
const parsedFields = parseFields(fieldsStr);
|
||||
const parsedFields = parseKVPairs(rest);
|
||||
|
||||
const unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; });
|
||||
if (unknownFields.length > 0) {
|
||||
@@ -655,6 +780,7 @@ jobs:
|
||||
await postComment('❌ **PocketBase Bot**: PATCH failed for `' + slug + '`:\n```\n' + patchRes.body + '\n```');
|
||||
process.exit(1);
|
||||
}
|
||||
await revalidate(slug);
|
||||
await addReaction('+1');
|
||||
const changesLines = Object.entries(payload)
|
||||
.map(function ([k, v]) { return '- `' + k + '` → `' + JSON.stringify(v) + '`'; })
|
||||
|
||||
1
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
1
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
@@ -170,7 +170,6 @@ jobs:
|
||||
website: data.website,
|
||||
logo: data.logo,
|
||||
description: data.description,
|
||||
config_path: data.config_path,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
|
||||
is_dev: false
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -442,6 +442,16 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-04-14
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- BentoPDF: Nginx fixes [@tremor021](https://github.com/tremor021) ([#13741](https://github.com/community-scripts/ProxmoxVE/pull/13741))
|
||||
- Zerobyte: add git to dependencies to fix bun install failure [@Copilot](https://github.com/Copilot) ([#13721](https://github.com/community-scripts/ProxmoxVE/pull/13721))
|
||||
- alpine-nextcloud-install: do not use deprecated nginx config [@AlexanderStein](https://github.com/AlexanderStein) ([#13726](https://github.com/community-scripts/ProxmoxVE/pull/13726))
|
||||
|
||||
## 2026-04-13
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
@@ -457,6 +467,10 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
- Bytestash: auto backup/restore data on update [@MickLesk](https://github.com/MickLesk) ([#13707](https://github.com/community-scripts/ProxmoxVE/pull/13707))
|
||||
- OpenCloud: pin version to 6.0.0 [@vhsdream](https://github.com/vhsdream) ([#13691](https://github.com/community-scripts/ProxmoxVE/pull/13691))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- Mealie: pin version to v3.14.0 in install and update scripts [@Copilot](https://github.com/Copilot) ([#13724](https://github.com/community-scripts/ProxmoxVE/pull/13724))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- core: remove unused TEMP_DIR mktemp leak in build_container / clean sonarqube [@MickLesk](https://github.com/MickLesk) ([#13708](https://github.com/community-scripts/ProxmoxVE/pull/13708))
|
||||
|
||||
@@ -28,7 +28,8 @@ function update_script() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
if check_for_gh_release "mealie" "mealie-recipes/mealie"; then
|
||||
RELEASE="v3.14.0"
|
||||
if check_for_gh_release "mealie" "mealie-recipes/mealie" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
NODE_MODULE="yarn" NODE_VERSION="24" setup_nodejs
|
||||
|
||||
@@ -40,7 +41,7 @@ function update_script() {
|
||||
cp -f /opt/mealie/mealie.env /opt/mealie.env
|
||||
msg_ok "Backup completed"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/opt/mealie"
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "${RELEASE}" "/opt/mealie"
|
||||
|
||||
msg_info "Installing Python Dependencies with uv"
|
||||
cd /opt/mealie
|
||||
|
||||
@@ -38,6 +38,7 @@ function update_script() {
|
||||
cp /opt/zerobyte/.env /opt/zerobyte.env.bak
|
||||
msg_ok "Backed up Configuration"
|
||||
|
||||
ensure_dependencies git
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "zerobyte" "nicotsx/zerobyte" "tarball"
|
||||
|
||||
|
||||
@@ -102,8 +102,9 @@ server {
|
||||
fastcgi_read_timeout 120s;
|
||||
}
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name localhost;
|
||||
root /usr/share/webapps/nextcloud;
|
||||
index index.php index.html index.htm;
|
||||
|
||||
@@ -38,9 +38,9 @@ msg_ok "Setup BentoPDF"
|
||||
msg_info "Creating Service"
|
||||
CERT_CN="$(hostname -I | awk '{print $1}')"
|
||||
$STD openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \
|
||||
-keyout /etc/ssl/private/bentopdf-selfsigned.key \
|
||||
-out /etc/ssl/certs/bentopdf-selfsigned.crt \
|
||||
-subj "/CN=${CERT_CN}"
|
||||
-keyout /etc/ssl/private/bentopdf-selfsigned.key \
|
||||
-out /etc/ssl/certs/bentopdf-selfsigned.crt \
|
||||
-subj "/CN=${CERT_CN}"
|
||||
|
||||
cat <<'EOF' >/etc/nginx/sites-available/bentopdf
|
||||
server {
|
||||
@@ -102,6 +102,10 @@ server {
|
||||
EOF
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
ln -sf /etc/nginx/sites-available/bentopdf /etc/nginx/sites-enabled/bentopdf
|
||||
systemctl stop nginx
|
||||
systemctl disable -q nginx
|
||||
sed -i '/application\/rss+xml/a\ application\/javascript mjs;' /etc/nginx/mime.types
|
||||
|
||||
cat <<'EOF' >/etc/systemd/system/bentopdf.service
|
||||
[Unit]
|
||||
Description=BentoPDF Service
|
||||
@@ -116,7 +120,6 @@ Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable -q --now bentopdf
|
||||
msg_ok "Created & started service"
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ msg_ok "Installed Dependencies"
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
PG_VERSION="16" setup_postgresql
|
||||
NODE_MODULE="yarn" NODE_VERSION="24" setup_nodejs
|
||||
fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/opt/mealie"
|
||||
fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "v3.14.0" "/opt/mealie"
|
||||
PG_DB_NAME="mealie_db" PG_DB_USER="mealie_user" PG_DB_GRANT_SUPERUSER="true" setup_postgresql_db
|
||||
|
||||
msg_info "Installing Python Dependencies with uv"
|
||||
|
||||
@@ -18,6 +18,7 @@ echo "davfs2 davfs2/suid_file boolean false" | debconf-set-selections
|
||||
$STD apt-get install -y \
|
||||
bzip2 \
|
||||
fuse3 \
|
||||
git \
|
||||
sshfs \
|
||||
davfs2 \
|
||||
openssh-client
|
||||
|
||||
Reference in New Issue
Block a user