mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
merge: bring origin/main Helm chart + CI updates into v6 consolidation branch
This commit is contained in:
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
What you expected to happen.
|
||||
|
||||
**Environment:**
|
||||
- Pulse Version: [e.g. v4.15.0]
|
||||
- Installation Type: [e.g. ProxmoxVE LXC, Docker, Manual]
|
||||
|
||||
**Additional context**
|
||||
Add any other context, screenshots, or logs here.
|
||||
|
||||
💡 **Tip:** If you're experiencing connection issues, API errors, or missing data, you can attach diagnostics from Settings → Diagnostics tab → Export for GitHub (sanitized version)
|
||||
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
102
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
name: Bug report
|
||||
description: Report a reproducible bug with full version details
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for reporting this. I need exact version metadata so I can triage quickly across rapid patch releases.
|
||||
|
||||
If your issue is connection- or data-related, include diagnostics from:
|
||||
`Settings -> Diagnostics -> Export for GitHub (sanitized)`.
|
||||
|
||||
- type: textarea
|
||||
id: bug_description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: What is broken, and what is the impact?
|
||||
placeholder: A clear and concise description of the problem.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Exact, minimal steps to reproduce the issue.
|
||||
placeholder: |
|
||||
1. Go to ...
|
||||
2. Click ...
|
||||
3. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What did you expect to happen instead?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: pulse_version
|
||||
attributes:
|
||||
label: Pulse version
|
||||
description: Exact version shown in the UI or logs.
|
||||
placeholder: v5.1.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: agent_version
|
||||
attributes:
|
||||
label: Agent version
|
||||
description: Exact agent version (or "none" if no agents are involved).
|
||||
placeholder: v5.1.5
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: image_ref
|
||||
attributes:
|
||||
label: Image tag or digest
|
||||
description: Exact image reference used by the running container.
|
||||
placeholder: rcourtman/pulse:5.1.5 or rcourtman/pulse@sha256:...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: install_type
|
||||
attributes:
|
||||
label: Installation type
|
||||
options:
|
||||
- Docker
|
||||
- Docker Compose
|
||||
- ProxmoxVE LXC
|
||||
- Kubernetes / Helm
|
||||
- Bare metal / systemd
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs or diagnostics
|
||||
description: Paste relevant logs, errors, or sanitized diagnostics export details.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: confirmation
|
||||
attributes:
|
||||
label: Confirmations
|
||||
options:
|
||||
- label: I verified the exact Pulse version and image reference from the running instance.
|
||||
required: true
|
||||
- label: I searched existing issues for duplicates.
|
||||
required: true
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Support
|
||||
url: https://github.com/rcourtman/Pulse/discussions
|
||||
about: Please ask and answer questions here
|
||||
- name: Documentation
|
||||
url: https://github.com/rcourtman/Pulse/wiki
|
||||
about: Check the wiki for guides and documentation
|
||||
about: Check the wiki for guides and documentation
|
||||
|
||||
200
.github/workflows/close-needs-retest-timeout.yml
vendored
Normal file
200
.github/workflows/close-needs-retest-timeout.yml
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
name: Close Needs-Retest Timeouts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 13 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stale_days:
|
||||
description: "Days to wait after retest request before auto-close"
|
||||
required: false
|
||||
default: "7"
|
||||
dry_run:
|
||||
description: "When true, do not close issues"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
close-timeouts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto-close outdated retest issues
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
STALE_DAYS: ${{ github.event.inputs.stale_days || '7' }}
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const RETEST_LABEL = "needs-retest-on-latest";
|
||||
const OPT_OUT_LABEL = "no-auto-close";
|
||||
const CLOSED_LABEL = "closed-needs-retest-timeout";
|
||||
const RETEST_COMMENT_MARKER = "<!-- issue-version-triage:v1 -->";
|
||||
const CLOSE_COMMENT_MARKER = "<!-- issue-timeout-close:v1 -->";
|
||||
|
||||
const staleDaysRaw = process.env.STALE_DAYS || "7";
|
||||
const staleDays = Number.parseInt(staleDaysRaw, 10);
|
||||
if (!Number.isFinite(staleDays) || staleDays < 1) {
|
||||
core.setFailed(`Invalid stale_days input: ${staleDaysRaw}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dryRun = String(process.env.DRY_RUN || "false").toLowerCase() === "true";
|
||||
const cutoffMs = Date.now() - staleDays * 24 * 60 * 60 * 1000;
|
||||
const cutoffIso = new Date(cutoffMs).toISOString();
|
||||
|
||||
core.info(`Running timeout close job with stale_days=${staleDays}, dry_run=${dryRun}`);
|
||||
core.info(`Cutoff timestamp: ${cutoffIso}`);
|
||||
|
||||
async function ensureLabel(name, color, description) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await ensureLabel(
|
||||
CLOSED_LABEL,
|
||||
"6e7781",
|
||||
"Auto-closed after retest request timeout without reporter follow-up"
|
||||
);
|
||||
|
||||
const candidates = await github.paginate(github.rest.issues.listForRepo, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: "open",
|
||||
labels: RETEST_LABEL,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
core.info(`Found ${candidates.length} open issues with ${RETEST_LABEL}`);
|
||||
|
||||
let closedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let dryRunCount = 0;
|
||||
|
||||
for (const issue of candidates) {
|
||||
if (issue.pull_request) {
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelSet = new Set((issue.labels || []).map((l) => l.name));
|
||||
if (labelSet.has(OPT_OUT_LABEL)) {
|
||||
core.info(`#${issue.number} skipped (${OPT_OUT_LABEL} present).`);
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const triageComments = comments.filter((c) => (c.body || "").includes(RETEST_COMMENT_MARKER));
|
||||
if (triageComments.length === 0) {
|
||||
core.info(`#${issue.number} skipped (no retest-request marker comment found).`);
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestTriage = triageComments.sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
)[triageComments.length - 1];
|
||||
|
||||
const triageAt = new Date(latestTriage.created_at).getTime();
|
||||
if (!Number.isFinite(triageAt) || triageAt > cutoffMs) {
|
||||
core.info(`#${issue.number} skipped (retest request is not stale yet).`);
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const authorLogin = issue.user?.login;
|
||||
const reporterReplied = comments.some((c) => {
|
||||
return c.user?.login === authorLogin && new Date(c.created_at).getTime() > triageAt;
|
||||
});
|
||||
|
||||
if (reporterReplied) {
|
||||
core.info(`#${issue.number} skipped (reporter followed up after retest request).`);
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const communityActivityAfterTriage = comments.some((c) => {
|
||||
const t = new Date(c.created_at).getTime();
|
||||
const isAfter = Number.isFinite(t) && t > triageAt;
|
||||
if (!isAfter) return false;
|
||||
const assoc = String(c.author_association || "");
|
||||
const isMaintainer = ["OWNER", "MEMBER", "COLLABORATOR"].includes(assoc);
|
||||
return !isMaintainer;
|
||||
});
|
||||
|
||||
if (communityActivityAfterTriage) {
|
||||
core.info(`#${issue.number} skipped (community activity exists after retest request).`);
|
||||
skippedCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const closeCommentBody = [
|
||||
CLOSE_COMMENT_MARKER,
|
||||
`Closing due to missing reporter retest confirmation for ${staleDays} days.`,
|
||||
"",
|
||||
"If this still reproduces on the latest stable release, comment with updated version details and logs and I will reopen.",
|
||||
].join("\n");
|
||||
|
||||
if (dryRun) {
|
||||
core.info(`[dry-run] Would close #${issue.number}`);
|
||||
dryRunCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: closeCommentBody,
|
||||
});
|
||||
|
||||
const newLabels = new Set([...labelSet]);
|
||||
newLabels.delete(RETEST_LABEL);
|
||||
newLabels.add(CLOSED_LABEL);
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [...newLabels].sort(),
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
});
|
||||
|
||||
core.info(`#${issue.number} closed due to retest timeout.`);
|
||||
closedCount += 1;
|
||||
}
|
||||
|
||||
core.notice(
|
||||
`Needs-retest timeout summary: closed=${closedCount}, dry_run=${dryRunCount}, skipped=${skippedCount}, total=${candidates.length}`
|
||||
);
|
||||
205
.github/workflows/issue-version-triage.yml
vendored
Normal file
205
.github/workflows/issue-version-triage.yml
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
name: Issue Version Triage
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- reopened
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
if: ${{ github.event.issue.pull_request == null }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply version labels and retest guidance
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
const labelNames = new Set((issue.labels || []).map((label) => label.name));
|
||||
|
||||
// Restrict automation to bug reports only.
|
||||
if (!labelNames.has("bug")) {
|
||||
core.info("Issue is not labeled bug. Skipping version triage.");
|
||||
return;
|
||||
}
|
||||
|
||||
const VERSION_LABEL_PREFIX = "affects-";
|
||||
const NEEDS_VERSION_LABEL = "needs-version-info";
|
||||
const RETEST_LABEL = "needs-retest-on-latest";
|
||||
const RETEST_COMMENT_MARKER = "<!-- issue-version-triage:v1 -->";
|
||||
|
||||
function normalizeVersion(value) {
|
||||
if (!value) return null;
|
||||
const match = String(value).match(/\bv?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function extractPulseVersion(body) {
|
||||
if (!body) return null;
|
||||
|
||||
const lines = body.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i] || "";
|
||||
if (/pulse\s*(\||-)?\s*version/i.test(line)) {
|
||||
const inlineVersion = normalizeVersion(line);
|
||||
if (inlineVersion) return inlineVersion;
|
||||
|
||||
// Check nearby lines because issue forms often put values on the next line.
|
||||
for (let j = i + 1; j < Math.min(i + 6, lines.length); j += 1) {
|
||||
const nearby = (lines[j] || "").trim();
|
||||
if (!nearby) continue;
|
||||
const nearbyVersion = normalizeVersion(nearby);
|
||||
if (nearbyVersion) return nearbyVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: issue-form block headed by "Pulse version".
|
||||
const headingMatch = body.match(/#+\s*Pulse version[\s\S]{0,80}?(\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b)/i);
|
||||
if (headingMatch) return normalizeVersion(headingMatch[1]);
|
||||
|
||||
// Final fallback for older templates with "Pulse | Version: [5.1.2]".
|
||||
const legacyMatch = body.match(/pulse\s*\|?\s*version[^\n]*?(\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\b)/i);
|
||||
if (legacyMatch) return normalizeVersion(legacyMatch[1]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCore(version) {
|
||||
const match = String(version || "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||
if (!match) return null;
|
||||
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
||||
}
|
||||
|
||||
function compareCore(a, b) {
|
||||
const av = parseCore(a);
|
||||
const bv = parseCore(b);
|
||||
if (!av || !bv) return null;
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (av[i] > bv[i]) return 1;
|
||||
if (av[i] < bv[i]) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function ensureLabel(name, color, description) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status !== 404) throw error;
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function hasRetestComment(issueNumber) {
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
per_page: 100,
|
||||
});
|
||||
return comments.some((comment) => (comment.body || "").includes(RETEST_COMMENT_MARKER));
|
||||
}
|
||||
|
||||
const reportedVersion = extractPulseVersion(issue.body);
|
||||
core.info(`Reported Pulse version: ${reportedVersion || "not found"}`);
|
||||
|
||||
let latestVersion = null;
|
||||
try {
|
||||
const latest = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
});
|
||||
latestVersion = normalizeVersion(latest.data.tag_name || latest.data.name || "");
|
||||
} catch (error) {
|
||||
core.warning(`Could not determine latest release: ${error.message}`);
|
||||
}
|
||||
core.info(`Latest stable release: ${latestVersion || "unknown"}`);
|
||||
|
||||
const nextLabels = new Set(labelNames);
|
||||
|
||||
if (reportedVersion) {
|
||||
await ensureLabel(
|
||||
`${VERSION_LABEL_PREFIX}${reportedVersion}`,
|
||||
"0e8a16",
|
||||
`Bug reported against Pulse ${reportedVersion}`
|
||||
);
|
||||
await ensureLabel(
|
||||
RETEST_LABEL,
|
||||
"d93f0b",
|
||||
"Reporter should retest on current latest stable release"
|
||||
);
|
||||
|
||||
// Keep only one exact affects-X.Y.Z label for clarity.
|
||||
for (const label of [...nextLabels]) {
|
||||
if (/^affects-\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(label) && label !== `${VERSION_LABEL_PREFIX}${reportedVersion}`) {
|
||||
nextLabels.delete(label);
|
||||
}
|
||||
}
|
||||
|
||||
nextLabels.add(`${VERSION_LABEL_PREFIX}${reportedVersion}`);
|
||||
nextLabels.delete(NEEDS_VERSION_LABEL);
|
||||
|
||||
const comparison = latestVersion ? compareCore(reportedVersion, latestVersion) : null;
|
||||
if (comparison !== null && comparison < 0) {
|
||||
nextLabels.add(RETEST_LABEL);
|
||||
|
||||
if (!(await hasRetestComment(issue.number))) {
|
||||
const body = [
|
||||
RETEST_COMMENT_MARKER,
|
||||
"Thanks for the report.",
|
||||
"",
|
||||
`I can see this was reported on **v${reportedVersion}**, while the latest stable release is **v${latestVersion}**.`,
|
||||
`Please retest on **v${latestVersion}** and comment with:`,
|
||||
"",
|
||||
"- whether the issue still reproduces",
|
||||
"- updated logs/diagnostics",
|
||||
"- exact running image tag or digest",
|
||||
"",
|
||||
"If there is no reporter follow-up after 7 days, this issue may be auto-closed until new confirmation is provided.",
|
||||
"",
|
||||
"If it still reproduces on the latest version, I will keep this open as an active regression.",
|
||||
].join("\n");
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
nextLabels.delete(RETEST_LABEL);
|
||||
}
|
||||
} else {
|
||||
await ensureLabel(
|
||||
NEEDS_VERSION_LABEL,
|
||||
"fbca04",
|
||||
"Issue is missing required Pulse version metadata"
|
||||
);
|
||||
nextLabels.add(NEEDS_VERSION_LABEL);
|
||||
nextLabels.delete(RETEST_LABEL);
|
||||
}
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [...nextLabels].sort(),
|
||||
});
|
||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
||||
name: pulse
|
||||
description: Helm chart for deploying the Pulse hub and optional Docker monitoring agent.
|
||||
type: application
|
||||
version: 5.1.4
|
||||
appVersion: "5.1.4"
|
||||
version: 5.1.7
|
||||
appVersion: "5.1.7"
|
||||
icon: https://raw.githubusercontent.com/rcourtman/Pulse/main/docs/images/pulse-logo.svg
|
||||
keywords:
|
||||
- monitoring
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# pulse
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
Helm chart for deploying the Pulse hub and optional Docker monitoring agent.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user