Compare commits

..

1 Commits

Author SHA1 Message Date
push-app-to-main[bot]
a492e8397b Add github-runner (ct) 2026-04-13 13:55:34 +00:00
23 changed files with 372 additions and 448 deletions

480
.github/workflows/pocketbase-bot.yml generated vendored
View File

@@ -31,8 +31,6 @@ 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 () {
@@ -115,6 +113,7 @@ 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');
@@ -129,11 +128,18 @@ 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;
@@ -141,8 +147,6 @@ 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' +
@@ -158,16 +162,12 @@ jobs:
'/pocketbase <slug> note edit <type> "<old text>" "<new text>"\n' +
'/pocketbase <slug> note remove <type> "<text>"\n' +
'```\n\n' +
'**Install method management:**\n' +
'**Install method resources:**\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 <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' +
'```\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,7 +189,8 @@ jobs:
process.exit(0);
}
// ── PocketBase: authenticate ───────────────────────────────────────
// ── Allowed fields and their types ─────────────────────────────────
// ── PocketBase: authenticate (shared by all paths) ─────────────────
const raw = process.env.POCKETBASE_URL.replace(/\/$/, '');
const apiBase = /\/api$/i.test(raw) ? raw : raw + '/api';
const coll = process.env.POCKETBASE_COLLECTION;
@@ -209,7 +210,7 @@ jobs:
}
const token = JSON.parse(authRes.body).token;
// ── PocketBase: find record by slug ────────────────────────────────
// ── PocketBase: find record by slug (shared by all paths) ──────────
const recordsUrl = apiBase + '/collections/' + encodeURIComponent(coll) + '/records';
const filter = "(slug='" + slug.replace(/'/g, "''") + "')";
const listRes = await request(recordsUrl + '?filter=' + encodeURIComponent(filter) + '&perPage=1', {
@@ -227,164 +228,57 @@ 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 (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 ──────────────────────────────────────────────
if (noteMatch) {
// ── NOTE SUBCOMMAND (reads/writes notes_json on script record) ────
const noteAction = noteMatch[1].toLowerCase();
const noteArgsStr = rest.substring(noteMatch[0].length).trim();
let notesArr = readJsonBlob(record.notes_json);
async function patchNotes(arr) {
// 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) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
@@ -392,7 +286,7 @@ jobs:
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update notes:\n```\n' + res.body + '\n```');
await postComment('❌ **PocketBase Bot**: Failed to update `notes_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
@@ -405,7 +299,7 @@ jobs:
);
} else if (noteAction === 'add') {
const tokens = parseTokens(noteArgsStr);
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
@@ -417,8 +311,7 @@ jobs:
const noteType = tokens[0].toLowerCase();
const noteText = tokens.slice(1).join(' ');
notesArr.push({ type: noteType, text: noteText });
await patchNotes(notesArr);
await revalidate(slug);
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Added note to **`' + slug + '`**\n\n' +
@@ -428,7 +321,7 @@ jobs:
);
} else if (noteAction === 'edit') {
const tokens = parseTokens(noteArgsStr);
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 3) {
await addReaction('-1');
await postComment(
@@ -453,8 +346,7 @@ jobs:
process.exit(0);
}
notesArr[idx].text = newText;
await patchNotes(notesArr);
await revalidate(slug);
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Edited note in **`' + slug + '`**\n\n' +
@@ -465,7 +357,7 @@ jobs:
);
} else if (noteAction === 'remove') {
const tokens = parseTokens(noteArgsStr);
const tokens = parseNoteTokens(noteArgsStr);
if (tokens.length < 2) {
await addReaction('-1');
await postComment(
@@ -489,8 +381,7 @@ jobs:
);
process.exit(0);
}
await patchNotes(notesArr);
await revalidate(slug);
await patchNotesJson(notesArr);
await addReaction('+1');
await postComment(
'✅ **PocketBase Bot**: Removed note from **`' + slug + '`**\n\n' +
@@ -501,36 +392,36 @@ jobs:
}
} else if (methodMatch) {
// ── METHOD SUBCOMMAND ────────────────────────────────────────────
// ── METHOD SUBCOMMAND (reads/writes install_methods_json on script record) ──
const methodArgs = rest.replace(/^method\s*/i, '').trim();
const methodListMode = !methodArgs || methodArgs.toLowerCase() === 'list';
let methodsArr = readJsonBlob(record.install_methods_json);
// 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);
// 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 = []; }
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;
}
}
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');
}
async function patchMethods(arr) {
async function patchInstallMethodsJson(arr) {
const res = await request(recordsUrl + '/' + record.id, {
method: 'PATCH',
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
body: JSON.stringify({ install_methods_json: arr })
body: JSON.stringify({ install_methods_json: JSON.stringify(arr) })
});
if (!res.ok) {
await addReaction('-1');
await postComment('❌ **PocketBase Bot**: Failed to update install methods:\n```\n' + res.body + '\n```');
await postComment('❌ **PocketBase Bot**: Failed to update `install_methods_json`:\n```\n' + res.body + '\n```');
process.exit(1);
}
}
@@ -541,122 +432,70 @@ jobs:
' **PocketBase Bot**: Install methods for **`' + slug + '`** (' + methodsArr.length + ' total)\n\n' +
formatMethodsList(methodsArr)
);
} else {
// 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');
// Parse: <type> cpu=N ram=N hdd=N
const methodParts = methodArgs.match(/^(\S+)\s+(.+)$/);
if (!methodParts) {
await addReaction('-1');
await postComment(
' **PocketBase Bot**: Added install method **`' + newType + '`** to **`' + slug + '`**\n\n' +
formatMethodsList([newMethod]) + '\n\n' +
'*Executed by @' + actor + '*'
);
} 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**: 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 + '*'
' **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```'
);
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 || '?'; });
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.'
);
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 (value from code block) ───────────────────────
// ── SET SUBCOMMAND (multi-line / HTML / special chars via code block) ──
const fieldName = setMatch[1].toLowerCase();
const SET_ALLOWED = {
name: 'string', description: 'string', logo: 'string',
@@ -692,7 +531,6 @@ 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(
@@ -703,6 +541,11 @@ 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',
@@ -725,7 +568,39 @@ jobs:
deleted_message: 'string',
};
const parsedFields = parseKVPairs(rest);
// 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 unknownFields = Object.keys(parsedFields).filter(function (f) { return !ALLOWED_FIELDS[f]; });
if (unknownFields.length > 0) {
@@ -780,7 +655,6 @@ 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) + '`'; })

View File

@@ -170,6 +170,7 @@ 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

View File

@@ -442,50 +442,14 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
</details>
## 2026-04-14
### 🚀 Updated Scripts
- Immich: Pin photo-processing library revisions [@vhsdream](https://github.com/vhsdream) ([#13748](https://github.com/community-scripts/ProxmoxVE/pull/13748))
- #### 🐞 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))
- #### ✨ New Features
- Mealie: support v3.15+ Nuxt 4 migration [@MickLesk](https://github.com/MickLesk) ([#13731](https://github.com/community-scripts/ProxmoxVE/pull/13731))
- #### 🔧 Refactor
- Lyrion: correct service name and version file in update script [@MickLesk](https://github.com/MickLesk) ([#13734](https://github.com/community-scripts/ProxmoxVE/pull/13734))
- Changedetection: move env vars from service file to .env [@tremor021](https://github.com/tremor021) ([#13732](https://github.com/community-scripts/ProxmoxVE/pull/13732))
## 2026-04-13
### 🚀 Updated Scripts
- #### 🐞 Bug Fixes
- Slskd: Remove stale Soularr lock file on startup and redirect logs to stderr [@MickLesk](https://github.com/MickLesk) ([#13669](https://github.com/community-scripts/ProxmoxVE/pull/13669))
- Bambuddy: preserve database and archive on update [@Copilot](https://github.com/Copilot) ([#13706](https://github.com/community-scripts/ProxmoxVE/pull/13706))
- #### ✨ New Features
- Immich: Pin version to 2.7.5 [@vhsdream](https://github.com/vhsdream) ([#13715](https://github.com/community-scripts/ProxmoxVE/pull/13715))
- 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))
## 2026-04-12
### 🚀 Updated Scripts

View File

@@ -39,9 +39,6 @@ function update_script() {
msg_info "Backing up Configuration and Data"
cp /opt/bambuddy/.env /opt/bambuddy.env.bak
cp -r /opt/bambuddy/data /opt/bambuddy_data_bak
[[ -f /opt/bambuddy/bambuddy.db ]] && cp /opt/bambuddy/bambuddy.db /opt/bambuddy.db.bak
[[ -f /opt/bambuddy/bambutrack.db ]] && cp /opt/bambuddy/bambutrack.db /opt/bambutrack.db.bak
[[ -d /opt/bambuddy/archive ]] && cp -r /opt/bambuddy/archive /opt/bambuddy_archive_bak
msg_ok "Backed up Configuration and Data"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bambuddy" "maziggy/bambuddy" "tarball" "latest" "/opt/bambuddy"
@@ -62,14 +59,8 @@ function update_script() {
mkdir -p /opt/bambuddy/data
cp /opt/bambuddy.env.bak /opt/bambuddy/.env
cp -r /opt/bambuddy_data_bak/. /opt/bambuddy/data/
[[ -f /opt/bambuddy.db.bak ]] && cp /opt/bambuddy.db.bak /opt/bambuddy/bambuddy.db
[[ -f /opt/bambutrack.db.bak ]] && cp /opt/bambutrack.db.bak /opt/bambuddy/bambutrack.db
if [[ -d /opt/bambuddy_archive_bak ]]; then
mkdir -p /opt/bambuddy/archive
cp -r /opt/bambuddy_archive_bak/. /opt/bambuddy/archive/
fi
rm -f /opt/bambuddy.env.bak /opt/bambuddy.db.bak /opt/bambutrack.db.bak
rm -rf /opt/bambuddy_data_bak /opt/bambuddy_archive_bak
rm -f /opt/bambuddy.env.bak
rm -rf /opt/bambuddy_data_bak
msg_ok "Restored Configuration and Data"
msg_info "Starting Service"

View File

@@ -29,41 +29,28 @@ function update_script() {
exit
fi
if check_for_gh_release "bytestash" "jordan-dalby/ByteStash"; then
msg_info "Stopping Services"
systemctl stop bytestash-backend bytestash-frontend
msg_ok "Services Stopped"
read -rp "${TAB3}Did you make a backup via application WebUI? (y/n): " backuped
if [[ "$backuped" =~ ^[Yy]$ ]]; then
msg_info "Stopping Services"
systemctl stop bytestash-backend bytestash-frontend
msg_ok "Services Stopped"
msg_info "Backing up data"
tmp_dir="/opt/bytestash-data-backup"
mkdir -p "$tmp_dir"
if [[ -d /opt/bytestash/data ]]; then
cp -r /opt/bytestash/data "$tmp_dir"/data
elif [[ -d /opt/data ]]; then
cp -r /opt/data "$tmp_dir"/data
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bytestash" "jordan-dalby/ByteStash" "tarball"
msg_info "Configuring ByteStash"
cd /opt/bytestash/server
$STD npm install
cd /opt/bytestash/client
$STD npm install
msg_ok "Updated ByteStash"
msg_info "Starting Services"
systemctl start bytestash-backend bytestash-frontend
msg_ok "Started Services"
else
msg_error "PLEASE MAKE A BACKUP FIRST!"
exit
fi
msg_ok "Data backed up"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "bytestash" "jordan-dalby/ByteStash" "tarball"
msg_info "Restoring data"
if [[ -d "$tmp_dir"/data ]]; then
mkdir -p /opt/bytestash/data
cp -r "$tmp_dir"/data/* /opt/bytestash/data/
rm -rf "$tmp_dir"
fi
msg_ok "Data restored"
msg_info "Configuring ByteStash"
cd /opt/bytestash/server
$STD npm install
cd /opt/bytestash/client
$STD npm install
msg_ok "Updated ByteStash"
msg_info "Starting Services"
systemctl start bytestash-backend bytestash-frontend
msg_ok "Started Services"
msg_ok "Updated successfully!"
fi
exit

71
ct/github-runner.sh Normal file
View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/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/actions/runner
APP="GitHub-Runner"
var_tags="${var_tags:-ci}"
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_unprivileged="${var_unprivileged:-1}"
var_nesting="${var_nesting:-1}"
var_keyctl="${var_keyctl:-1}"
header_info "$APP"
variables
color
catch_errors
function update_script() {
header_info
check_container_storage
check_container_resources
if [[ ! -f /opt/actions-runner/run.sh ]]; then
msg_error "No ${APP} Installation Found!"
exit 1
fi
if check_for_gh_release "actions-runner" "actions/runner"; then
msg_info "Stopping Service"
systemctl stop actions-runner
msg_ok "Stopped Service"
msg_info "Backing up runner configuration"
BACKUP_DIR="/opt/actions-runner.backup"
mkdir -p "$BACKUP_DIR"
for f in .runner .credentials .credentials_rsaparams .env .path; do
[[ -f /opt/actions-runner/$f ]] && cp -a /opt/actions-runner/$f "$BACKUP_DIR/"
done
msg_ok "Backed up configuration"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-x64-*.tar.gz"
msg_info "Restoring runner configuration"
for f in .runner .credentials .credentials_rsaparams .env .path; do
[[ -f "$BACKUP_DIR/$f" ]] && cp -a "$BACKUP_DIR/$f" /opt/actions-runner/
done
rm -rf "$BACKUP_DIR"
msg_ok "Restored configuration"
msg_info "Starting Service"
systemctl start actions-runner
msg_ok "Started Service"
msg_ok "Updated successfully!"
fi
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} After first boot, run config.sh with your token and start the service.${CL}"

View File

@@ -109,7 +109,7 @@ EOF
msg_ok "Image-processing libraries up to date"
fi
RELEASE="v2.7.5"
RELEASE="v2.7.4"
if check_for_gh_release "Immich" "immich-app/immich" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
if [[ $(cat ~/.immich) > "2.5.1" ]]; then
msg_info "Enabling Maintenance Mode"
@@ -309,8 +309,7 @@ function compile_libjxl() {
SOURCE=${SOURCE_DIR}/libjxl
JPEGLI_LIBJPEG_LIBRARY_SOVERSION="62"
JPEGLI_LIBJPEG_LIBRARY_VERSION="62.3.0"
LIBJXL_REVISION="794a5dcf0d54f9f0b20d288a12e87afb91d20dfc"
# : "${LIBJXL_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libjxl.json)}"
: "${LIBJXL_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libjxl.json)}"
if [[ "$LIBJXL_REVISION" != "$(grep 'libjxl' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
msg_info "Recompiling libjxl"
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"
@@ -354,8 +353,7 @@ function compile_libjxl() {
function compile_libheif() {
SOURCE=${SOURCE_DIR}/libheif
ensure_dependencies libaom-dev
LIBHEIF_REVISION="35dad50a9145332a7bfdf1ff6aef6801fb613d68"
# : "${LIBHEIF_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libheif.json)}"
: "${LIBHEIF_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libheif.json)}"
if [[ "${update:-}" ]] || [[ "$LIBHEIF_REVISION" != "$(grep 'libheif' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
msg_info "Recompiling libheif"
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"
@@ -386,8 +384,7 @@ function compile_libheif() {
function compile_libraw() {
SOURCE=${SOURCE_DIR}/libraw
LIBRAW_REVISION="0b56545a4f828743f28a4345cdfdd4c49f9f9a2a"
# : "${LIBRAW_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libraw.json)}"
: "${LIBRAW_REVISION:=$(jq -cr '.revision' "$BASE_DIR"/server/sources/libraw.json)}"
if [[ "$LIBRAW_REVISION" != "$(grep 'libraw' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
msg_info "Recompiling libraw"
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"

View File

@@ -30,16 +30,16 @@ function update_script() {
exit
fi
DEB_URL=$(curl_with_retry 'https://lyrion.org/getting-started/' | grep -oP '<a\s[^>]*href="\K[^"]*amd64\.deb(?="[^>]*>)' | head -n 1)
DEB_URL=$(curl -s 'https://lyrion.org/getting-started/' | grep -oP '<a\s[^>]*href="\K[^"]*amd64\.deb(?="[^>]*>)' | head -n 1)
RELEASE=$(echo "$DEB_URL" | grep -oP 'lyrionmusicserver_\K[0-9.]+(?=_amd64\.deb)')
DEB_FILE="/tmp/lyrionmusicserver_${RELEASE}_amd64.deb"
if [[ ! -f /opt/lyrion_version.txt ]] || [[ "${RELEASE}" != "$(cat /opt/lyrion_version.txt)" ]]; then
msg_info "Updating $APP to ${RELEASE}"
curl_with_retry "$DEB_URL" "$DEB_FILE"
curl -fsSL -o "$DEB_FILE" "$DEB_URL"
$STD apt install "$DEB_FILE" -y
systemctl restart lyrionmusicserver
rm -f "$DEB_FILE"
echo "${RELEASE}" >/opt/lyrion_version.txt
systemctl restart lyrion
$STD rm -f "$DEB_FILE"
echo "${RELEASE}" >/opt/${APP}_version.txt
msg_ok "Updated $APP to ${RELEASE}"
msg_ok "Updated successfully!"
else

View File

@@ -40,7 +40,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"
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/opt/mealie"
msg_info "Installing Python Dependencies with uv"
cd /opt/mealie
@@ -49,10 +49,9 @@ function update_script() {
msg_info "Building Frontend"
MEALIE_VERSION=$(<$HOME/.mealie)
SITE_SETTINGS=$(find /opt/mealie/frontend -name "site-settings.vue" -path "*/admin/*" | head -1)
$STD sed -i "s|https://github.com/mealie-recipes/mealie/commit/|https://github.com/mealie-recipes/mealie/releases/tag/|g" "$SITE_SETTINGS"
$STD sed -i "s|value: data.buildId,|value: \"v${MEALIE_VERSION}\",|g" "$SITE_SETTINGS"
$STD sed -i "s|value: data.production ? i18n.t(\"about.production\") : i18n.t(\"about.development\"),|value: \"bare-metal\",|g" "$SITE_SETTINGS"
$STD sed -i "s|https://github.com/mealie-recipes/mealie/commit/|https://github.com/mealie-recipes/mealie/releases/tag/|g" /opt/mealie/frontend/pages/admin/site-settings.vue
$STD sed -i "s|value: data.buildId,|value: \"v${MEALIE_VERSION}\",|g" /opt/mealie/frontend/pages/admin/site-settings.vue
$STD sed -i "s|value: data.production ? i18n.t(\"about.production\") : i18n.t(\"about.development\"),|value: \"bare-metal\",|g" /opt/mealie/frontend/pages/admin/site-settings.vue
export NUXT_TELEMETRY_DISABLED=1
cd /opt/mealie/frontend
$STD yarn install --prefer-offline --frozen-lockfile --non-interactive --production=false --network-timeout 1000000
@@ -98,3 +97,4 @@ 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 "${TAB}${GATEWAY}${BGN}http://${IP}:9000${CL}"

View File

@@ -43,7 +43,6 @@ function update_script() {
RELEASE=$(get_latest_github_release "SonarSource/sonarqube")
curl -fsSL "https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-${RELEASE}.zip" -o $temp_file
unzip -q "$temp_file" -d /opt
rm -f "$temp_file"
mv /opt/sonarqube-${RELEASE} /opt/sonarqube
echo "${RELEASE}" > ~/.sonarqube
msg_ok "Updated SonarQube"

View File

@@ -38,7 +38,6 @@ 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"

View File

@@ -102,9 +102,8 @@ server {
fastcgi_read_timeout 120s;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name localhost;
root /usr/share/webapps/nextcloud;
index index.php index.html index.htm;

View File

@@ -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,10 +102,6 @@ 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
@@ -120,6 +116,7 @@ Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q --now bentopdf
msg_ok "Created & started service"

View File

@@ -56,10 +56,6 @@ NODE_VERSION="24" setup_nodejs
msg_info "Installing Change Detection"
mkdir /opt/changedetection
$STD pip3 install changedetection.io
cat <<EOF >/opt/changedetection/.env
WEBDRIVER_URL=http://127.0.0.1:4444/wd/hub
PLAYWRIGHT_DRIVER_URL=ws://localhost:3000/chrome?launch=eyJkZWZhdWx0Vmlld3BvcnQiOnsiaGVpZ2h0Ijo3MjAsIndpZHRoIjoxMjgwfSwiaGVhZGxlc3MiOmZhbHNlLCJzdGVhbHRoIjp0cnVlfQ==&blockAds=true
EOF
msg_ok "Installed Change Detection"
msg_info "Installing Browserless & Playwright"
@@ -116,13 +112,12 @@ Description=Change Detection
After=network-online.target
After=network.target browserless.service
Wants=browserless.service
[Service]
Type=simple
EnvironmentFile=/opt/changedetection/.env
WorkingDirectory=/opt/changedetection
Environment=WEBDRIVER_URL=http://127.0.0.1:4444/wd/hub
Environment=PLAYWRIGHT_DRIVER_URL=ws://localhost:3000/chrome?launch=eyJkZWZhdWx0Vmlld3BvcnQiOnsiaGVpZ2h0Ijo3MjAsIndpZHRoIjoxMjgwfSwiaGVhZGxlc3MiOmZhbHNlLCJzdGVhbHRoIjp0cnVlfQ==&blockAds=true
ExecStart=changedetection.io -d /opt/changedetection -p 5000
[Install]
WantedBy=multi-user.target
EOF
@@ -131,16 +126,15 @@ cat <<EOF >/etc/systemd/system/browserless.service
[Unit]
Description=browserless service
After=network.target
[Service]
Environment=CONNECTION_TIMEOUT=60000
WorkingDirectory=/opt/browserless
ExecStart=/opt/browserless/scripts/start.sh
SyslogIdentifier=browserless
[Install]
WantedBy=default.target
EOF
systemctl enable -q --now browserless
systemctl enable -q --now changedetection
msg_ok "Created Services"

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Copyright (c) 2021-2026 community-scripts ORG
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
# Source: https://docs.github.com/en/actions/hosting-your-own-runners
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
color
verb_ip6
catch_errors
setting_up_container
network_check
update_os
msg_info "Installing Dependencies"
$STD apt install -y \
git \
gh
msg_ok "Installed Dependencies"
NODE_VERSION="24" setup_nodejs
msg_info "Creating runner user (no sudo)"
if ! getent passwd runner >/dev/null 2>&1; then
useradd -m -s /bin/bash runner
fi
msg_ok "Runner user ready"
fetch_and_deploy_gh_release "actions-runner" "actions/runner" "prebuild" "latest" "/opt/actions-runner" "actions-runner-linux-x64-*.tar.gz"
msg_info "Setting ownership for runner user"
chown -R runner:runner /opt/actions-runner
msg_ok "Ownership set"
msg_info "Creating Service"
cat <<EOF >/etc/systemd/system/actions-runner.service
[Unit]
Description=GitHub Actions self-hosted runner
Documentation=https://docs.github.com/en/actions/hosting-your-own-runners
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=runner
WorkingDirectory=/opt/actions-runner
ExecStart=/opt/actions-runner/run.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl enable -q actions-runner
msg_ok "Created Service"
motd_ssh
customize
cleanup_lxc

View File

@@ -175,8 +175,7 @@ cd "$STAGING_DIR"
SOURCE=${SOURCE_DIR}/libjxl
JPEGLI_LIBJPEG_LIBRARY_SOVERSION="62"
JPEGLI_LIBJPEG_LIBRARY_VERSION="62.3.0"
LIBJXL_REVISION="794a5dcf0d54f9f0b20d288a12e87afb91d20dfc"
# : "${LIBJXL_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libjxl.json)}"
: "${LIBJXL_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libjxl.json)}"
$STD git clone https://github.com/libjxl/libjxl.git "$SOURCE"
cd "$SOURCE"
$STD git reset --hard "$LIBJXL_REVISION"
@@ -213,8 +212,7 @@ msg_ok "(1/5) Compiled libjxl"
msg_info "(2/5) Compiling libheif"
SOURCE=${SOURCE_DIR}/libheif
LIBHEIF_REVISION="35dad50a9145332a7bfdf1ff6aef6801fb613d68"
# : "${LIBHEIF_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libheif.json)}"
: "${LIBHEIF_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libheif.json)}"
$STD git clone https://github.com/strukturag/libheif.git "$SOURCE"
cd "$SOURCE"
$STD git reset --hard "$LIBHEIF_REVISION"
@@ -239,8 +237,7 @@ msg_ok "(2/5) Compiled libheif"
msg_info "(3/5) Compiling libraw"
SOURCE=${SOURCE_DIR}/libraw
LIBRAW_REVISION="0b56545a4f828743f28a4345cdfdd4c49f9f9a2a"
# : "${LIBRAW_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libraw.json)}"
: "${LIBRAW_REVISION:=$(jq -cr '.revision' $BASE_DIR/server/sources/libraw.json)}"
$STD git clone https://github.com/LibRaw/LibRaw.git "$SOURCE"
cd "$SOURCE"
$STD git reset --hard "$LIBRAW_REVISION"
@@ -298,7 +295,7 @@ ML_DIR="${APP_DIR}/machine-learning"
GEO_DIR="${INSTALL_DIR}/geodata"
mkdir -p {"${APP_DIR}","${UPLOAD_DIR}","${GEO_DIR}","${INSTALL_DIR}"/cache}
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.7.5" "$SRC_DIR"
fetch_and_deploy_gh_release "Immich" "immich-app/immich" "tarball" "v2.7.4" "$SRC_DIR"
PNPM_VERSION="$(jq -r '.packageManager | split("@")[1] | split("+")[0]' ${SRC_DIR}/package.json)"
NODE_VERSION="24" NODE_MODULE="pnpm@${PNPM_VERSION}" setup_nodejs

View File

@@ -14,10 +14,10 @@ network_check
update_os
msg_info "Setup Lyrion Music Server"
DEB_URL=$(curl_with_retry 'https://lyrion.org/getting-started/' | grep -oP '<a\s[^>]*href="\K[^"]*amd64\.deb(?="[^>]*>)' | head -n 1)
DEB_URL=$(curl -fsSL 'https://lyrion.org/getting-started/' | grep -oP '<a\s[^>]*href="\K[^"]*amd64\.deb(?="[^>]*>)' | head -n 1)
RELEASE=$(echo "$DEB_URL" | grep -oP 'lyrionmusicserver_\K[0-9.]+(?=_amd64\.deb)')
DEB_FILE="/tmp/lyrionmusicserver_${RELEASE}_amd64.deb"
curl_with_retry "$DEB_URL" "$DEB_FILE"
curl -fsSL -o "$DEB_FILE" "$DEB_URL"
$STD apt install "$DEB_FILE" -y
rm -f "$DEB_FILE"
echo "${RELEASE}" >"/opt/lyrion_version.txt"

View File

@@ -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"
fetch_and_deploy_gh_release "mealie" "mealie-recipes/mealie" "tarball" "latest" "/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"
@@ -42,10 +42,9 @@ msg_info "Building Frontend"
MEALIE_VERSION=$(<$HOME/.mealie)
export NUXT_TELEMETRY_DISABLED=1
cd /opt/mealie/frontend
SITE_SETTINGS=$(find /opt/mealie/frontend -name "site-settings.vue" -path "*/admin/*" | head -1)
$STD sed -i "s|https://github.com/mealie-recipes/mealie/commit/|https://github.com/mealie-recipes/mealie/releases/tag/|g" "$SITE_SETTINGS"
$STD sed -i "s|value: data.buildId,|value: \"v${MEALIE_VERSION}\",|g" "$SITE_SETTINGS"
$STD sed -i "s|value: data.production ? i18n.t(\"about.production\") : i18n.t(\"about.development\"),|value: \"bare-metal\",|g" "$SITE_SETTINGS"
$STD sed -i "s|https://github.com/mealie-recipes/mealie/commit/|https://github.com/mealie-recipes/mealie/releases/tag/|g" /opt/mealie/frontend/pages/admin/site-settings.vue
$STD sed -i "s|value: data.buildId,|value: \"v${MEALIE_VERSION}\",|g" /opt/mealie/frontend/pages/admin/site-settings.vue
$STD sed -i "s|value: data.production ? i18n.t(\"about.production\") : i18n.t(\"about.development\"),|value: \"bare-metal\",|g" /opt/mealie/frontend/pages/admin/site-settings.vue
$STD yarn install --prefer-offline --frozen-lockfile --non-interactive --production=false --network-timeout 1000000
$STD yarn generate
msg_ok "Built Frontend"

View File

@@ -48,15 +48,12 @@ if [[ ${soularr,,} =~ ^(y|yes)$ ]]; then
#!/usr/bin/env bash
if ps aux | grep "[s]oularr.py" >/dev/null; then
echo "Soularr is already running. Exiting..." >&2
echo "Soularr is already running. Exiting..."
exit 1
else
source /opt/soularr/venv/bin/activate
uv run python3 -u /opt/soularr/soularr.py --config-dir /opt/soularr
fi
# Remove stale lock file from previous ungraceful exit
rm -f "/opt/soularr/.soularr.lock"
source /opt/soularr/venv/bin/activate
uv run python3 -u /opt/soularr/soularr.py --config-dir /opt/soularr 2>&1
EOF
chmod +x /opt/soularr/run.sh
deactivate

View File

@@ -21,7 +21,6 @@ temp_file=$(mktemp)
RELEASE=$(get_latest_github_release "SonarSource/sonarqube")
curl -fsSL "https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-${RELEASE}.zip" -o $temp_file
unzip -q "$temp_file" -d /opt
rm -f "$temp_file"
mv /opt/sonarqube-* /opt/sonarqube
$STD useradd -r -m -U -d /opt/sonarqube -s /bin/bash sonarqube
chown -R sonarqube:sonarqube /opt/sonarqube

View File

@@ -18,7 +18,6 @@ echo "davfs2 davfs2/suid_file boolean false" | debconf-set-selections
$STD apt-get install -y \
bzip2 \
fuse3 \
git \
sshfs \
davfs2 \
openssh-client

View File

@@ -3613,6 +3613,8 @@ build_container() {
fi
# Build PCT_OPTIONS as string for export
TEMP_DIR=$(mktemp -d)
pushd "$TEMP_DIR" >/dev/null
local _func_url
if [ "$var_os" == "alpine" ]; then
_func_url="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/alpine-install.func"

View File

@@ -317,7 +317,7 @@ function default_settings() {
# Determine available network modes based on bridge count
local DEFAULT_WAN_BRG
DEFAULT_WAN_BRG=$(echo "$AVAILABLE_BRIDGES" | grep -v "^${BRG}$" | head -n1 || true)
DEFAULT_WAN_BRG=$(echo "$AVAILABLE_BRIDGES" | grep -v "^${BRG}$" | head -n1)
if [ "$BRIDGE_COUNT" -ge 2 ]; then
# Multiple bridges available - offer dual or single mode
@@ -509,7 +509,7 @@ function advanced_settings() {
# Build WAN bridge selection from available bridges (excluding LAN bridge)
local WAN_BRIDGES
WAN_BRIDGES=$(get_available_bridges | grep -v "^${BRG}$" || true)
WAN_BRIDGES=$(get_available_bridges | grep -v "^${BRG}$")
if [ -z "$WAN_BRIDGES" ]; then
msg_error "No additional bridge available for WAN. Only '${BRG}' exists."
msg_error "Create a second bridge (e.g. vmbr1) in Proxmox network config first."
@@ -738,8 +738,8 @@ done
msg_info "Creating a OPNsense VM"
qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
-name $HN -tags community-script -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
pvesm alloc $STORAGE $VMID $DISK0 4M &>/dev/null
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} &>/dev/null
pvesm alloc $STORAGE $VMID $DISK0 4M 1>&/dev/null
qm importdisk $VMID ${FILE} $STORAGE ${DISK_IMPORT:-} 1>&/dev/null
qm set $VMID \
-efidisk0 ${DISK0_REF}${FORMAT} \
-scsi0 ${DISK1_REF},${DISK_CACHE}${THIN}size=2G \