mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-11-12 18:25:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd5bd55b8b |
No files matched your search
Generated
+4
-149
@@ -2,7 +2,7 @@ name: PocketBase Bot
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
@@ -25,9 +25,6 @@ jobs:
|
||||
POCKETBASE_ADMIN_PASSWORD: ${{ secrets.POCKETBASE_ADMIN_PASSWORD }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
EVENT_ACTION: ${{ github.event.action }}
|
||||
SENDER: ${{ github.event.sender.login }}
|
||||
PREVIOUS_BODY: ${{ github.event.changes.body.from }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || 'false' }}
|
||||
REPO_OWNER: ${{ github.repository_owner }}
|
||||
@@ -90,7 +87,7 @@ jobs:
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER, 10);
|
||||
const isPrComment = process.env.IS_PR_COMMENT === 'true';
|
||||
const commentId = parseInt(process.env.COMMENT_ID, 10);
|
||||
let actor = process.env.ACTOR;
|
||||
const actor = process.env.ACTOR;
|
||||
|
||||
function ghRequest(path, method, body) {
|
||||
const headers = {
|
||||
@@ -281,65 +278,7 @@ jobs:
|
||||
// Accept /pocketbase at the start of ANY line (leading whitespace ok),
|
||||
// so the command works even when preceded by other text. Mid-sentence
|
||||
// mentions and blockquoted ("> ...") examples are ignored.
|
||||
let commentBody = process.env.COMMENT_BODY || '';
|
||||
|
||||
// ── Metadata requests from the website ─────────────────────────────
|
||||
// One comment per requested change, each with Accept/Decline boxes.
|
||||
// Ticking one edits the comment; that edit is the only event handled.
|
||||
const REQUEST_MARKER = '<!-- pb-request -->';
|
||||
const DONE_MARKER = '<!-- pb-request:done -->';
|
||||
const isRequest = commentBody.includes(REQUEST_MARKER);
|
||||
let decidedBy = null;
|
||||
if (process.env.EVENT_ACTION === 'edited' && !isRequest) process.exit(0);
|
||||
if (isRequest) {
|
||||
if (process.env.EVENT_ACTION !== 'edited' || commentBody.includes(DONE_MARKER)) process.exit(0);
|
||||
const ticked = (body, word) =>
|
||||
new RegExp('^[ \\t]*[-*] \\[[xX]\\] ' + word + '[ \\t]*\\r?$', 'm').test(body);
|
||||
const previous = process.env.PREVIOUS_BODY || '';
|
||||
const accept = ticked(commentBody, 'Accept') && !ticked(previous, 'Accept');
|
||||
const decline = ticked(commentBody, 'Decline') && !ticked(previous, 'Decline');
|
||||
if (accept === decline) process.exit(0);
|
||||
|
||||
const sender = process.env.SENDER;
|
||||
const permRes = await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/collaborators/' + encodeURIComponent(sender) + '/permission'
|
||||
);
|
||||
const level = permRes.ok ? JSON.parse(permRes.body).permission : 'none';
|
||||
if (level !== 'admin' && level !== 'write') {
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: @' + sender + ' is not authorized to decide metadata requests.\n' +
|
||||
'Only maintainers with write access can accept or decline them.'
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const heading = (commentBody.match(/^### (.+)$/m) || [])[1] || 'this change';
|
||||
const command = commentBody.match(/<!-- pb-command -->\r?\n([\s\S]*?)\r?\n<!-- \/pb-command -->/);
|
||||
const decided = commentBody
|
||||
.replace(/^[ \t]*[-*] \[[ xX]\] (Accept|Decline)[ \t]*\r?\n?/gm, '')
|
||||
.replace(
|
||||
REQUEST_MARKER,
|
||||
'**' + (accept ? '✅ Accepted' : '❌ Declined') + '** by @' + sender + '\n\n' +
|
||||
REQUEST_MARKER + '\n' + DONE_MARKER
|
||||
);
|
||||
const markRes = await ghRequest(
|
||||
'/repos/' + owner + '/' + repo + '/issues/comments/' + commentId, 'PATCH', { body: decided }
|
||||
);
|
||||
if (!markRes.ok) console.warn('Could not mark the request as decided:', markRes.body);
|
||||
|
||||
if (decline) {
|
||||
await postComment('❌ **PocketBase Bot**: **' + heading + '** declined by @' + sender + '.');
|
||||
process.exit(0);
|
||||
}
|
||||
if (!command) {
|
||||
await postComment('❌ **PocketBase Bot**: no command found in the request for **' + heading + '**.');
|
||||
process.exit(0);
|
||||
}
|
||||
commentBody = command[1];
|
||||
decidedBy = sender;
|
||||
actor = sender;
|
||||
}
|
||||
|
||||
const commentBody = process.env.COMMENT_BODY || '';
|
||||
const cmdLine = commentBody
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
@@ -353,7 +292,7 @@ jobs:
|
||||
|
||||
// ── Permission check ───────────────────────────────────────────────
|
||||
const association = process.env.ACTOR_ASSOCIATION;
|
||||
if (!decidedBy && association !== 'OWNER' && association !== 'MEMBER') {
|
||||
if (association !== 'OWNER' && association !== 'MEMBER') {
|
||||
await addReaction('-1');
|
||||
await postComment(
|
||||
'❌ **PocketBase Bot**: @' + actor + ' is not authorized to use this command.\n' +
|
||||
@@ -1018,35 +957,6 @@ jobs:
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
if (fieldName === 'app_vars') {
|
||||
const TYPES = ['text', 'password', 'number', 'boolean', 'select'];
|
||||
const problems = [];
|
||||
if (!Array.isArray(parsedJson)) problems.push('must be a JSON array');
|
||||
else parsedJson.forEach(function (v, i) {
|
||||
const at = 'entry ' + (i + 1) + ': ';
|
||||
if (!v || typeof v !== 'object' || Array.isArray(v)) { problems.push(at + 'must be an object'); return; }
|
||||
if (typeof v.name !== 'string' || !/^var_[a-z0-9_]+$/.test(v.name)) problems.push(at + '`name` must look like `var_example`');
|
||||
if (typeof v.label !== 'string' || !v.label.trim()) problems.push(at + '`label` is required');
|
||||
if (TYPES.indexOf(v.type) === -1) problems.push(at + '`type` must be one of ' + TYPES.join(', '));
|
||||
if (v.default !== undefined && typeof v.default !== 'string') problems.push(at + '`default` must be a string');
|
||||
if (v.type === 'select') {
|
||||
const opts = v.options;
|
||||
if (!Array.isArray(opts) || opts.length === 0 || opts.some(function (o) { return typeof o !== 'string' || !o; })) {
|
||||
problems.push(at + 'a select needs `options`, a non-empty array of strings');
|
||||
} else if (v.default !== undefined && opts.indexOf(v.default) === -1) {
|
||||
problems.push(at + '`default` must be one of its options');
|
||||
}
|
||||
}
|
||||
['required', 'secret'].forEach(function (k) {
|
||||
if (v[k] !== undefined && typeof v[k] !== 'boolean') problems.push(at + '`' + k + '` must be true or false');
|
||||
});
|
||||
});
|
||||
if (problems.length > 0) {
|
||||
await addReaction('-1');
|
||||
await postComment('❌ **PocketBase Bot**: `app_vars` was not saved.\n\n- ' + problems.join('\n- '));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
setPayload[fieldName] = parsedJson;
|
||||
} else {
|
||||
setPayload[fieldName] = codeBlockValue;
|
||||
@@ -1326,58 +1236,3 @@ jobs:
|
||||
});
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
|
||||
- name: Close a metadata request once every change is decided
|
||||
if: always() && github.event.action == 'edited' && contains(github.event.comment.body, '<!-- pb-request -->')
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
node << 'ENDSCRIPT'
|
||||
(async function () {
|
||||
const api = 'https://api.github.com/repos/' + process.env.REPO + '/issues/' + process.env.ISSUE_NUMBER;
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + process.env.GITHUB_TOKEN,
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'pocketbase-bot'
|
||||
};
|
||||
const https = require('https');
|
||||
function call(method, url, body) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const req = https.request(url, { method: method, headers: headers }, function (res) {
|
||||
let data = '';
|
||||
res.on('data', function (d) { data += d; });
|
||||
res.on('end', function () { resolve({ ok: res.statusCode < 300, status: res.statusCode, body: data }); });
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
const requests = [];
|
||||
for (let page = 1; page <= 10; page++) {
|
||||
const res = await call('GET', api + '/comments?per_page=100&page=' + page);
|
||||
if (!res.ok) throw new Error('Could not list comments: ' + res.status);
|
||||
const comments = JSON.parse(res.body);
|
||||
for (const c of comments) {
|
||||
if ((c.body || '').includes('<!-- pb-request -->')) requests.push(c.body);
|
||||
}
|
||||
if (comments.length < 100) break;
|
||||
}
|
||||
const open = requests.filter(function (b) { return !b.includes('<!-- pb-request:done -->'); });
|
||||
if (requests.length === 0 || open.length > 0) {
|
||||
console.log(open.length + ' of ' + requests.length + ' changes still undecided.');
|
||||
return;
|
||||
}
|
||||
const accepted = requests.filter(function (b) { return b.includes('✅ Accepted'); }).length;
|
||||
const res = await call('PATCH', api, { state: 'closed', state_reason: accepted > 0 ? 'completed' : 'not_planned' });
|
||||
if (!res.ok) throw new Error('Could not close the issue: ' + res.status);
|
||||
console.log('Closed: ' + accepted + ' accepted, ' + (requests.length - accepted) + ' declined.');
|
||||
})().catch(function (e) {
|
||||
console.error(e.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
ENDSCRIPT
|
||||
shell: bash
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs ci-vm-controller.sh on the hypervisor (prox1): the script, the
|
||||
# token reader, a systemd unit per CI VM, and each VM's config. Run as root
|
||||
# on prox1 from a directory that also holds ci-vm-controller.sh:
|
||||
#
|
||||
# scp ci-controller-setup.sh ci-vm-controller.sh root@prox1:/root/ci-controller/
|
||||
# ssh root@prox1 'cd /root/ci-controller && bash ci-controller-setup.sh'
|
||||
#
|
||||
# It does not start anything: a VM goes into service with
|
||||
# systemctl enable --now ci-vm-controller@<vmid>
|
||||
# once it has a 'clean' snapshot without any runner registered (see
|
||||
# docs/guides/pr-script-testing.md, "Single-use hosts").
|
||||
#
|
||||
# The token (/etc/ci-controller/token) is put there by hand: a fine-grained
|
||||
# token for the two repositories with Administration: read and write only.
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
[[ -f ci-vm-controller.sh ]] || { echo "ci-vm-controller.sh missing next to this script" >&2; exit 1; }
|
||||
command -v jq >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq jq >/dev/null
|
||||
|
||||
install -m 755 ci-vm-controller.sh /usr/local/sbin/ci-vm-controller.sh
|
||||
install -d -m 700 /etc/ci-controller
|
||||
|
||||
cat >/usr/local/sbin/ci-gh-token <<'EOF'
|
||||
#!/bin/sh
|
||||
# Prints the token ci-vm-controller.sh uses (Administration: write on the CI
|
||||
# repositories). Swap this for a GitHub App token later without touching
|
||||
# the controller.
|
||||
exec cat /etc/ci-controller/token
|
||||
EOF
|
||||
chmod 755 /usr/local/sbin/ci-gh-token
|
||||
|
||||
cat >/etc/systemd/system/ci-vm-controller@.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Single-use CI runner host, VM %i
|
||||
After=network-online.target pve-cluster.service pvedaemon.service
|
||||
Wants=network-online.target
|
||||
# A missing token is a setup error, not something to retry every 30 s.
|
||||
ConditionPathExists=/etc/ci-controller/token
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/sbin/ci-vm-controller.sh %i
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
# Stopping the controller leaves the VM as it is; the next start resets it,
|
||||
# which also ends a job that may still be running there.
|
||||
KillMode=mixed
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# One repository per host: see ci-vm-controller.sh.
|
||||
conf() { # conf <vmid> <ip> <name> <label> <repo>
|
||||
cat >"/etc/ci-controller/$1.conf" <<EOF
|
||||
VM_IP=$2
|
||||
HOST_NAME=$3
|
||||
LABEL=$4
|
||||
REPOS=$5
|
||||
EOF
|
||||
}
|
||||
conf 200 10.201.146.232 pve-ci proxmox-test community-scripts/DevScripts
|
||||
conf 201 10.201.146.231 pve-ci-2 proxmox-test community-scripts/ProxmoxVE
|
||||
conf 202 10.201.146.233 incus-ci incus-test community-scripts/DevScripts
|
||||
conf 203 10.201.146.234 incus-ci-2 incus-test community-scripts/ProxmoxVE
|
||||
|
||||
systemctl daemon-reload
|
||||
echo "Installed. Token: $([[ -s /etc/ci-controller/token ]] && echo present || echo 'MISSING (/etc/ci-controller/token)')"
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Keeps one CI VM (pve-ci, incus-ci, ...) as a single-use GitHub runner host.
|
||||
# Runs on the hypervisor (prox1), which PR code never touches.
|
||||
#
|
||||
# Every cycle:
|
||||
# 1. roll the VM back to its clean snapshot and boot it;
|
||||
# 2. register a just-in-time runner on it (generate-jitconfig) for the
|
||||
# host's repository. Such a runner takes exactly one job and is deleted
|
||||
# by GitHub afterwards, so its credentials are worthless once that job
|
||||
# is over;
|
||||
# 3. (with several repositories in REPOS: as soon as one runner is busy,
|
||||
# delete the others that are still idle, so no second job starts on a
|
||||
# host that is about to be reset);
|
||||
# 4. once every runner of this cycle is gone, start over at 1.
|
||||
#
|
||||
# PR code runs as root on the VM, so nothing the VM says is trusted: when a
|
||||
# job is over is read from GitHub's API, never from the VM.
|
||||
#
|
||||
# Config: /etc/ci-controller/<vmid>.conf (or the environment), see below.
|
||||
# Usage: ci-vm-controller.sh <vmid> run forever (the systemd unit)
|
||||
# ONE_CYCLE=1 ci-vm-controller.sh <vmid> one job, then stop (testing)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VMID=${1:?usage: ci-vm-controller.sh <vmid>}
|
||||
CONF=${CONF:-/etc/ci-controller/$VMID.conf}
|
||||
# shellcheck source=/dev/null
|
||||
[[ -f "$CONF" ]] && . "$CONF"
|
||||
|
||||
: "${VM_IP:?VM_IP}" # address of the CI VM
|
||||
: "${HOST_NAME:?HOST_NAME}" # runner name prefix, e.g. pve-ci
|
||||
: "${LABEL:?LABEL}" # proxmox-test or incus-test
|
||||
# One repository per host. Several would each have a runner waiting on the
|
||||
# VM, and a job of one could read the idle runner's JIT config of the other
|
||||
# and use it elsewhere before it is withdrawn: one repository's PR code
|
||||
# faking the other's results. (Several are fine within one trust domain.)
|
||||
: "${REPOS:?REPOS, e.g. community-scripts/DevScripts}"
|
||||
EXTRA_LABELS=${EXTRA_LABELS:-} # space-separated, e.g. for a test
|
||||
SNAPSHOT=${SNAPSHOT:-clean}
|
||||
TOKEN_CMD=${TOKEN_CMD:-/usr/local/sbin/ci-gh-token} # prints a token with repo Administration: write
|
||||
PVE=${PVE:-} # prefix for qm, e.g. "ssh root@prox1" when not run on it
|
||||
GUEST_SSH=${GUEST_SSH:-"ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new root@$VM_IP"}
|
||||
POLL=${POLL:-20}
|
||||
MAX_BUSY_MIN=${MAX_BUSY_MIN:-180} # the test job's own limit is 90
|
||||
MAX_IDLE_MIN=${MAX_IDLE_MIN:-720} # recycle an idle host twice a day
|
||||
ONE_CYCLE=${ONE_CYCLE:-}
|
||||
|
||||
log() { echo "[$(date -u +%FT%TZ)] vm $VMID: $*"; }
|
||||
qm() { if [[ -n "$PVE" ]]; then $PVE qm "$@"; else command qm "$@"; fi; }
|
||||
|
||||
# Not in a subshell, so the status and the cached token survive the call:
|
||||
# api METHOD PATH [JSON] -> body in $API_BODY, HTTP status in $API_STATUS.
|
||||
API_FILE=$(mktemp)
|
||||
trap 'rm -f "$API_FILE"' EXIT
|
||||
TOKEN="" TOKEN_AT=0
|
||||
api() {
|
||||
local method=$1 path=$2 data=${3:-}
|
||||
# Cached for 30 minutes (an App installation token lives 60).
|
||||
if [[ -z "$TOKEN" || $(($(date +%s) - TOKEN_AT)) -gt 1800 ]]; then
|
||||
TOKEN=$($TOKEN_CMD) || { log "no GitHub token"; API_STATUS=000 API_BODY=""; return 1; }
|
||||
TOKEN_AT=$(date +%s)
|
||||
fi
|
||||
API_STATUS=$(curl -sS -o "$API_FILE" -w '%{http_code}' -X "$method" \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' ${data:+-d "$data"} "https://api.github.com$path") || API_STATUS=000
|
||||
API_BODY=$(<"$API_FILE")
|
||||
[[ "$API_STATUS" != 000 ]]
|
||||
}
|
||||
|
||||
reset_vm() {
|
||||
log "rolling back to '$SNAPSHOT'"
|
||||
[[ "$(qm status "$VMID")" == *running* ]] && qm stop "$VMID" --timeout 60 >/dev/null
|
||||
qm rollback "$VMID" "$SNAPSHOT" >/dev/null
|
||||
qm start "$VMID" >/dev/null
|
||||
local i
|
||||
for i in $(seq 1 60); do
|
||||
$GUEST_SSH true 2>/dev/null && { log "booted"; return 0; }
|
||||
sleep 5
|
||||
done
|
||||
log "no SSH after 5 minutes"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parallel arrays: one entry per runner registered in this cycle.
|
||||
R_REPO=() R_ID=() R_GONE=()
|
||||
|
||||
register() { # register <owner/repo>
|
||||
local repo=$1 name dir labels body cfg id
|
||||
name="$HOST_NAME-${repo#*/}-$(date +%s)"
|
||||
dir=/opt/actions-runner/${repo#*/}
|
||||
labels=$(printf '%s\n' "$LABEL" $EXTRA_LABELS | jq -R . | jq -sc .)
|
||||
api POST "/repos/$repo/actions/runners/generate-jitconfig" \
|
||||
"$(jq -nc --arg n "$name" --argjson l "$labels" '{name: $n, runner_group_id: 1, labels: $l, work_folder: "_work"}')" || return 1
|
||||
body=$API_BODY
|
||||
[[ "$API_STATUS" == 201 ]] || { log "$repo: generate-jitconfig failed ($API_STATUS): $(jq -r .message <<<"$body" 2>/dev/null)"; return 1; }
|
||||
id=$(jq -r .runner.id <<<"$body")
|
||||
cfg=$(jq -r .encoded_jit_config <<<"$body")
|
||||
# The config goes in through stdin, not the command line.
|
||||
if ! $GUEST_SSH "umask 077; cat >/run/ci-jit-${repo#*/} && systemd-run --quiet --collect --unit=ci-jit-${repo#*/} \
|
||||
-p WorkingDirectory=$dir -E RUNNER_ALLOW_RUNASROOT=1 \
|
||||
/bin/sh -c 'cfg=\$(cat /run/ci-jit-${repo#*/}); rm -f /run/ci-jit-${repo#*/}; exec ./run.sh --jitconfig \"\$cfg\"'" <<<"$cfg"; then
|
||||
log "$repo: could not start the runner on the VM"
|
||||
api DELETE "/repos/$repo/actions/runners/$id" >/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
R_REPO+=("$repo") R_ID+=("$id") R_GONE+=(0)
|
||||
log "$repo: runner $name (id $id) started"
|
||||
}
|
||||
|
||||
drop() { # drop <index>: delete an idle runner; GitHub refuses a busy one
|
||||
local i=$1
|
||||
api DELETE "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" >/dev/null || return 1
|
||||
if [[ "$API_STATUS" == 204 || "$API_STATUS" == 404 ]]; then R_GONE[$i]=1; return 0; fi
|
||||
return 1
|
||||
}
|
||||
|
||||
watch_cycle() {
|
||||
local start busy_since="" i body status busy any_busy left offline_since=""
|
||||
start=$(date +%s)
|
||||
while :; do
|
||||
sleep "$POLL"
|
||||
any_busy=0 left=0 offline=0
|
||||
for i in "${!R_ID[@]}"; do
|
||||
[[ "${R_GONE[$i]}" == 1 ]] && continue
|
||||
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || { left=1; continue; }
|
||||
body=$API_BODY
|
||||
if [[ "$API_STATUS" == 404 ]]; then
|
||||
R_GONE[$i]=1
|
||||
log "${R_REPO[$i]}: runner ${R_ID[$i]} is gone (job done or removed)"
|
||||
continue
|
||||
fi
|
||||
left=1
|
||||
busy=$(jq -r .busy <<<"$body"); status=$(jq -r .status <<<"$body")
|
||||
[[ "$busy" == true ]] && any_busy=1
|
||||
[[ "$status" == offline ]] && offline=1
|
||||
done
|
||||
if [[ "$any_busy" == 1 && -z "$busy_since" ]]; then
|
||||
busy_since=$(date +%s)
|
||||
log "a job started; withdrawing the idle runners"
|
||||
fi
|
||||
# A host that has started a job takes no other one: it will be reset.
|
||||
if [[ -n "$busy_since" ]]; then
|
||||
for i in "${!R_ID[@]}"; do
|
||||
[[ "${R_GONE[$i]}" == 1 ]] && continue
|
||||
api GET "/repos/${R_REPO[$i]}/actions/runners/${R_ID[$i]}" || continue
|
||||
body=$API_BODY
|
||||
[[ "$API_STATUS" == 200 && "$(jq -r .busy <<<"$body")" == false ]] && drop "$i" &&
|
||||
log "${R_REPO[$i]}: idle runner ${R_ID[$i]} withdrawn"
|
||||
done
|
||||
fi
|
||||
[[ "$left" == 0 ]] && { log "cycle over"; return 0; }
|
||||
|
||||
local now; now=$(date +%s)
|
||||
if [[ -n "$busy_since" && $((now - busy_since)) -gt $((MAX_BUSY_MIN * 60)) ]]; then
|
||||
log "job still running after $MAX_BUSY_MIN minutes; resetting anyway"
|
||||
break
|
||||
fi
|
||||
if [[ -z "$busy_since" && $((now - start)) -gt $((MAX_IDLE_MIN * 60)) ]]; then
|
||||
log "idle for $MAX_IDLE_MIN minutes; recycling"
|
||||
break
|
||||
fi
|
||||
# A runner that never comes online (or dies while idle) holds the host
|
||||
# forever otherwise.
|
||||
if [[ -z "$busy_since" && "$offline" == 1 ]]; then
|
||||
[[ -z "$offline_since" ]] && offline_since=$now
|
||||
if [[ $((now - offline_since)) -gt 300 ]]; then log "a runner is offline for 5 minutes; resetting"; break; fi
|
||||
else
|
||||
offline_since=""
|
||||
fi
|
||||
done
|
||||
for i in "${!R_ID[@]}"; do [[ "${R_GONE[$i]}" == 1 ]] || drop "$i" || true; done
|
||||
}
|
||||
|
||||
# Registrations of this host that a stopped or crashed controller left behind:
|
||||
# their VM was reset, so they are offline and never take a job, but they
|
||||
# clutter the runner list. Busy or online ones are left alone.
|
||||
cleanup_stale() {
|
||||
local repo ids id
|
||||
for repo in $REPOS; do
|
||||
api GET "/repos/$repo/actions/runners?per_page=100" || continue
|
||||
[[ "$API_STATUS" == 200 ]] || continue
|
||||
ids=$(jq -r --arg p "$HOST_NAME-${repo#*/}-" \
|
||||
'.runners[] | select((.name | startswith($p)) and .status == "offline" and (.busy | not)) | .id' <<<"$API_BODY")
|
||||
for id in $ids; do
|
||||
api DELETE "/repos/$repo/actions/runners/$id" && log "$repo: stale runner $id removed"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
while :; do
|
||||
R_REPO=() R_ID=() R_GONE=()
|
||||
if reset_vm; then
|
||||
cleanup_stale
|
||||
ok=0
|
||||
for repo in $REPOS; do register "$repo" && ok=1 || true; done
|
||||
if [[ "$ok" == 1 ]]; then watch_cycle; else log "no runner could be registered"; sleep 60; fi
|
||||
else
|
||||
sleep 60
|
||||
fi
|
||||
[[ -n "$ONE_CYCLE" ]] && { log "ONE_CYCLE: stopping; the VM is left as the job left it"; break; }
|
||||
done
|
||||
@@ -6,8 +6,6 @@
|
||||
#
|
||||
# incus-ci-host-setup.sh host only
|
||||
# incus-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
# incus-ci-host-setup.sh <owner/repo> jit host + runner software only, for
|
||||
# ci-vm-controller.sh (single-use host)
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
@@ -178,13 +176,6 @@ if [[ $# -eq 2 ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
|
||||
# a fresh runner for every job and resets the VM afterwards, so only the
|
||||
# runner software goes on it, and the 'clean' snapshot is taken after this.
|
||||
if [[ "$token" == jit ]]; then
|
||||
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
|
||||
exit 0
|
||||
fi
|
||||
if [[ -f .runner ]]; then
|
||||
./svc.sh stop >/dev/null 2>&1 || true
|
||||
./svc.sh uninstall >/dev/null 2>&1 || true
|
||||
|
||||
-9
@@ -5,8 +5,6 @@
|
||||
#
|
||||
# pve-ci-host-setup.sh host only
|
||||
# pve-ci-host-setup.sh <owner/repo> <token> host + a runner for that repo
|
||||
# pve-ci-host-setup.sh <owner/repo> jit host + runner software only, for
|
||||
# ci-vm-controller.sh (single-use host)
|
||||
#
|
||||
# The token is a runner *registration* token (valid one hour):
|
||||
# gh api -X POST repos/<owner/repo>/actions/runners/registration-token --jq .token
|
||||
@@ -198,13 +196,6 @@ if [[ $# -eq 2 ]]; then
|
||||
curl -fsSL "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" | tar -xz
|
||||
./bin/installdependencies.sh >/dev/null
|
||||
fi
|
||||
# `jit`: a single-use host. ci-vm-controller.sh on the hypervisor registers
|
||||
# a fresh runner for every job and resets the VM afterwards, so only the
|
||||
# runner software goes on it, and the 'clean' snapshot is taken after this.
|
||||
if [[ "$token" == jit ]]; then
|
||||
echo "runner software for ${repo} installed; ci-vm-controller.sh registers a runner per job"
|
||||
exit 0
|
||||
fi
|
||||
# A registered runner is reconfigured from scratch: stop and remove the old
|
||||
# registration (the token works for removal too).
|
||||
if [[ -f .runner ]]; then
|
||||
|
||||
+2
-4
@@ -555,9 +555,7 @@ check_health screenshot.png || fail "$health_problem"
|
||||
# only say "No update available" and never run update_script's real path. The
|
||||
# version files the install wrote (~/.<app>, one per fetch_and_deploy_gh_release)
|
||||
# that update_script checks are set back to 0.0.0-ci first; check_for_gh_release then sees an update and
|
||||
# runs it all. Apps deployed from a branch (fetch_and_deploy_gh_branch, checked
|
||||
# with check_for_gh_branch) store a commit hash instead of a version; the
|
||||
# engine compares both as plain strings, so 0.0.0-ci works for them too. `update` is the /usr/bin/update the install generated: it pulls
|
||||
# runs it all. `update` is the /usr/bin/update the install generated: it pulls
|
||||
# ct/<app>.sh from the URL this container was built from, i.e. the PR's tree.
|
||||
UPDATE_FAKE="0.0.0-ci"
|
||||
update_fail() {
|
||||
@@ -570,7 +568,7 @@ version_files=$(guest_exec sh -c '
|
||||
for f in /root/.*; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in */.install-* | *.pending | *.bak | */.bash* | */.profile | */.hushlogin | */.viminfo | */.lesshst | */.wget-hsts | */.selected_editor) continue ;; esac
|
||||
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^(v?[0-9]+(\.[0-9A-Za-z_+-]+)+|[0-9a-f]{7,40})$" "$f" && echo "$f"
|
||||
[ "$(wc -l <"$f")" -le 1 ] && grep -qE "^v?[0-9]+(\.[0-9A-Za-z_+-]+)+$" "$f" && echo "$f"
|
||||
done' 2>/dev/null | grep -E '^/root/\.[A-Za-z0-9._-]+$')
|
||||
# Only set back what update_script checks: the names it passes to
|
||||
# check_for_*release. The install can also deploy helpers the update never
|
||||
|
||||
@@ -11,8 +11,8 @@ var_tags="${var_tags:-web}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-1024}"
|
||||
var_disk="${var_disk:-8}"
|
||||
var_os="${var_os:-ubuntu}"
|
||||
var_version="${var_version:-24.04}"
|
||||
var_os="${var_os:-Debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-0}"
|
||||
|
||||
|
||||
Reference in new issue
Block a user