merge: bring origin/main Helm chart + CI updates into v6 consolidation branch

This commit is contained in:
rcourtman
2026-02-13 21:26:53 +00:00
7 changed files with 512 additions and 34 deletions

View File

@@ -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
View 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

View File

@@ -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

View 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}`
);

View 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(),
});

View File

@@ -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

View File

@@ -1,6 +1,6 @@
# pulse
![Version: 5.1.3](https://img.shields.io/badge/Version-5.1.3-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 5.1.3](https://img.shields.io/badge/AppVersion-5.1.3-informational?style=flat-square)
![Version: 5.1.6](https://img.shields.io/badge/Version-5.1.6-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 5.1.6](https://img.shields.io/badge/AppVersion-5.1.6-informational?style=flat-square)
Helm chart for deploying the Pulse hub and optional Docker monitoring agent.