[BUG] WashData tile card does not respect entity Display Precision setting #641
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Issue Validator | |
| on: | |
| issues: | |
| types: [opened, edited, reopened] | |
| issue_comment: | |
| types: [created] | |
| workflow_dispatch: | |
| inputs: | |
| issue_body: | |
| description: "Paste a simulated issue body to validate (dry-run - nothing is posted)" | |
| required: true | |
| issue_number: | |
| description: "Real issue number to post the result to (leave blank for dry-run only)" | |
| required: false | |
| default: "" | |
| permissions: | |
| issues: write | |
| jobs: | |
| handle-fine-command: | |
| runs-on: ubuntu-latest | |
| if: > | |
| github.event_name == 'issue_comment' && | |
| startsWith(github.event.comment.body, '/fine') && | |
| (github.event.comment.author_association == 'OWNER' || | |
| github.event.comment.author_association == 'MEMBER' || | |
| github.event.comment.author_association == 'COLLABORATOR') | |
| steps: | |
| - name: Handle /fine command | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issueNumber = context.payload.issue.number; | |
| const incompleteLabel = 'more info required'; | |
| const currentLabels = context.payload.issue.labels.map(l => l.name); | |
| if (currentLabels.includes(incompleteLabel)) { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| name: incompleteLabel, | |
| }); | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: `✅ Cleared by @${context.payload.comment.user.login} - issue marked as fine and queued for review.`, | |
| }); | |
| handle-reporter-response: | |
| runs-on: ubuntu-latest | |
| # When the reporter responds via a comment (instead of editing the issue body), | |
| # transition the issue from "more info required" to "awaiting maintainer review" | |
| # so the auto-closer doesn't close it as unanswered. The validator can't re-check | |
| # comment content (it inspects body fields), so a maintainer reviews instead. | |
| if: > | |
| github.event_name == 'issue_comment' && | |
| github.event.comment.user.type != 'Bot' && | |
| github.event.comment.user.login == github.event.issue.user.login && | |
| !startsWith(github.event.comment.body, '/fine') && | |
| contains(github.event.issue.labels.*.name, 'more info required') | |
| steps: | |
| - name: Swap labels on reporter response | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issueNumber = context.payload.issue.number; | |
| const incompleteLabel = 'more info required'; | |
| const reviewLabel = 'awaiting maintainer review'; | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| name: incompleteLabel, | |
| }).catch(e => console.log(`Could not remove ${incompleteLabel}: ${e.message}`)); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| labels: [reviewLabel], | |
| }); | |
| console.log(`#${issueNumber}: reporter responded via comment, swapped to "${reviewLabel}".`); | |
| validate-bug-report: | |
| runs-on: ubuntu-latest | |
| if: > | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'issues' && | |
| contains(github.event.issue.labels.*.name, 'bug')) | |
| steps: | |
| - name: Validate bug report completeness | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const isDryRun = context.eventName === 'workflow_dispatch'; | |
| const issue = isDryRun ? { | |
| body: context.payload.inputs.issue_body, | |
| number: parseInt(context.payload.inputs.issue_number) || null, | |
| user: { login: 'dry-run-user', type: 'User' }, | |
| labels: [], | |
| } : context.payload.issue; | |
| const body = issue.body || ''; | |
| const incompleteLabel = 'more info required'; | |
| // ── Checks ──────────────────────────────────────────────────────────── | |
| const problems = []; | |
| // 1. Device Brand & Model - must not be blank or the placeholder | |
| const brandMatch = body.match(/###\s*Device Brand & Model\s*\n+([^\n#]+)/); | |
| const brand = brandMatch ? brandMatch[1].trim() : ''; | |
| const brandIsNA = /^n\/?a$/i.test(brand); | |
| if (!brand || brand === 'Brand Model Number' || brand === 'Brand Model Number or N/A' || brand === '_No response_') { | |
| problems.push('**Device Brand & Model** is missing or still set to the placeholder value. Provide the actual brand and model (e.g. `Miele W1 WCG370`), or enter `N/A` if this bug is not related to a specific device.'); | |
| } | |
| // 2. WashData version - must not be blank or placeholder; warn if outdated | |
| const versionMatch = body.match(/###\s*WashData Integration Version\s*\n+([^\n#]+)/); | |
| const version = versionMatch ? versionMatch[1].trim() : ''; | |
| if (!version || version === 'vX.Y.Z' || version === '_No response_') { | |
| problems.push('**WashData Integration Version** is missing. You can find it in HACS or Settings → Integrations.'); | |
| } else { | |
| try { | |
| const releasesResp = await github.rest.repos.listReleases({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 10, | |
| }); | |
| const published = releasesResp.data.filter(r => !r.draft); | |
| const latestStable = published.find(r => !r.prerelease); | |
| const latestPre = published.find(r => r.prerelease); | |
| // Parse "1.2.3" or "1.2.3-alpha" into { parts: [1,2,3], isPre: bool } | |
| const parseVer = v => { | |
| const s = v.replace(/^v/i, '').trim().toLowerCase(); | |
| const [base, ...rest] = s.split('-'); | |
| return { parts: base.split('.').map(Number), isPre: rest.length > 0 }; | |
| }; | |
| // Returns true only when releaseTag is strictly newer than reportedStr | |
| const isNewer = (releaseTag, reportedStr) => { | |
| const rel = parseVer(releaseTag); | |
| const rep = parseVer(reportedStr); | |
| const len = Math.max(rel.parts.length, rep.parts.length); | |
| for (let i = 0; i < len; i++) { | |
| const a = rel.parts[i] || 0, b = rep.parts[i] || 0; | |
| if (a > b) return true; | |
| if (a < b) return false; | |
| } | |
| // Same numeric base: stable is newer than a pre-release of the same version | |
| return !rel.isPre && rep.isPre; | |
| }; | |
| // A pre-release is only a valid upgrade target when it is numerically | |
| // newer than the latest stable. If stable is ahead (or equal), it is | |
| // always the one true latest and the pre-release is never suggested. | |
| const alphaIsLatest = latestPre | |
| ? (!latestStable || isNewer(latestPre.tag_name, latestStable.tag_name)) | |
| : false; | |
| const suggestStable = latestStable && isNewer(latestStable.tag_name, version); | |
| const suggestPre = alphaIsLatest && latestPre && isNewer(latestPre.tag_name, version); | |
| if (suggestStable || suggestPre) { | |
| const parts = []; | |
| if (suggestStable) parts.push(`\`${latestStable.tag_name}\` (stable)`); | |
| if (suggestPre) parts.push(`\`${latestPre.tag_name}\` (pre-release)`); | |
| problems.push( | |
| `**Outdated version** (\`${version}\`): the current release${parts.length > 1 ? 's are' : ' is'} ${parts.join(' and ')}. ` + | |
| `Please upgrade and confirm the bug still exists on the latest version before we investigate — ` + | |
| `it may already be fixed. If it still reproduces, update this field with the version you tested.` | |
| ); | |
| } | |
| } catch (releaseErr) { | |
| console.log('Could not fetch releases for version check:', releaseErr.message); | |
| } | |
| } | |
| // 3. HA version - must not be blank or the placeholder | |
| const haVersionMatch = body.match(/###\s*Home Assistant Version\s*\n+([^\n#]+)/); | |
| const haVersion = haVersionMatch ? haVersionMatch[1].trim() : ''; | |
| if (!haVersion || haVersion === '2024.12.0' || haVersion === '_No response_') { | |
| problems.push('**Home Assistant Version** is missing. You can find it in Settings → About.'); | |
| } | |
| // 4. Logs / Error Evidence - must contain actual log/error output, an attached file, or a clear N/A with stated reason | |
| const logMatch = body.match(/###\s*Logs \/ Error Evidence\s*\n+([\s\S]*?)(?=\n###|$)/); | |
| const logContent = logMatch ? logMatch[1].trim() : ''; | |
| const logIsExplainedNA = /^n\/?a/i.test(logContent); | |
| // Detect GitHub file attachments (pasted as markdown links) | |
| const hasAttachment = /https:\/\/github\.com\/[^)]+\/files\//.test(logContent) || | |
| /https:\/\/github\.com\/user-attachments\//.test(logContent); | |
| const hasLogs = logContent && | |
| logContent !== '_No response_' && | |
| logContent !== 'Paste log output or error messages here, or describe what file(s) you have attached above...' && | |
| (logContent.length > 50 || hasAttachment); | |
| if (!hasLogs && !logIsExplainedNA) { | |
| problems.push( | |
| '**Logs / Error Evidence** are missing or appear empty. This is the most important piece of information for diagnosing bugs. ' + | |
| 'Please paste any relevant logs, error messages, or console output — debug logs if you have them, but even a single error line from the HA logbook is helpful. ' + | |
| 'See the **Logs / Error Evidence** section of the issue form for instructions on enabling debug logging. ' + | |
| 'If logs are **genuinely not applicable** to this report (for example: a documentation typo, a translation string fix, or a pure UI/config-flow layout issue where no runtime code path is exercised), ' + | |
| 'replace the placeholder with `N/A` followed by a short one-line reason — for example: `N/A - typo in README, no runtime behavior involved`. ' + | |
| 'Do not write `N/A` just because collecting logs is inconvenient; bug reports without logs and without a valid N/A reason cannot be investigated.' | |
| ); | |
| } | |
| // ── Label management ───────────────────────────────────────────────── | |
| const currentLabels = issue.labels.map(l => l.name); | |
| const hasIncompleteLabel = currentLabels.includes(incompleteLabel); | |
| const targetIssueNumber = isDryRun ? issue.number : issue.number; | |
| const canPost = !isDryRun || (isDryRun && targetIssueNumber); | |
| if (problems.length > 0) { | |
| const problemList = problems.map(p => `- ${p}`).join('\n'); | |
| const comment = `👋 Hey @${issue.user.login}, thanks for the report!\n\nLooks like a few things are missing that we need before we can investigate:\n\n${problemList}\n\nPlease update the issue with the missing information. Without it, we won't be able to reproduce or debug the problem, and the issue may be closed.\n\n> **How to edit this issue:** click the three-dot menu (⋯) at the top-right of your post → *Edit*.\n\n_This check runs automatically. Once the issue is updated, it will be re-evaluated._`; | |
| if (isDryRun) { | |
| console.log('🧪 DRY RUN - validation result: INCOMPLETE'); | |
| console.log('Problems found:\n' + problemList); | |
| console.log('Comment that would be posted:\n' + comment); | |
| if (!canPost) process.exit(0); | |
| } | |
| if (canPost) { | |
| // Add label if not already present | |
| if (!hasIncompleteLabel) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: targetIssueNumber, | |
| labels: [incompleteLabel], | |
| }); | |
| } | |
| // Only post a new comment if we haven't already (avoid comment spam on edits) | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: targetIssueNumber, | |
| }); | |
| const botComments = comments.data.filter( | |
| c => c.user.type === 'Bot' && (c.body || '').includes('missing that we need before we can investigate') | |
| ); | |
| if (botComments.length === 0) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: targetIssueNumber, | |
| body: comment, | |
| }); | |
| } else { | |
| // Update the existing bot comment instead | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComments[0].id, | |
| body: comment, | |
| }); | |
| } | |
| } | |
| } else if (hasIncompleteLabel || isDryRun) { | |
| if (isDryRun) { | |
| console.log('✅ DRY RUN - validation result: COMPLETE (all required fields present)'); | |
| if (!canPost) process.exit(0); | |
| } | |
| if (!isDryRun && hasIncompleteLabel) { | |
| // Everything looks good - remove the label and leave a positive note | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: incompleteLabel, | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: `Thanks for filling in the missing details, @${issue.user.login}! The issue looks complete now and has been queued for review. 🙌`, | |
| }); | |
| } | |
| } |