()
| 111 | }; |
| 112 | |
| 113 | const performCheck = async (): Promise<void> => { |
| 114 | if (settings.updates.tier === 'off') return; |
| 115 | // Coalesce overlapping ticks. performCheck mutates shared in-memory state and writes |
| 116 | // it to disk; concurrent runs would race on saveState() and could double-send emails. |
| 117 | if (checkInFlight) return; |
| 118 | checkInFlight = true; |
| 119 | try { |
| 120 | // getCurrentState() can throw on a non-ENOENT fs error from loadState(); |
| 121 | // it must run inside the try/finally so checkInFlight is always cleared, |
| 122 | // otherwise a one-time permission error permanently disables polling. |
| 123 | const state = await getCurrentState(); |
| 124 | const result = await checkLatestRelease({ |
| 125 | fetcher: realFetcher, |
| 126 | prevEtag: state.lastEtag, |
| 127 | repo: settings.updates.githubRepo, |
| 128 | }); |
| 129 | const now = new Date(); |
| 130 | state.lastCheckAt = now.toISOString(); |
| 131 | |
| 132 | if (result.kind === 'updated') { |
| 133 | state.latest = result.release; |
| 134 | state.lastEtag = result.etag; |
| 135 | } else if (result.kind === 'skipped-prerelease') { |
| 136 | // Preserve ETag so we don't re-fetch an unchanged prerelease body next tick. |
| 137 | state.lastEtag = result.etag; |
| 138 | } else if (result.kind === 'notmodified') { |
| 139 | // 304 — no state change. |
| 140 | } else if (result.kind === 'ratelimited') { |
| 141 | logger.warn('GitHub rate-limited; will retry at next interval'); |
| 142 | } else if (result.kind === 'error') { |
| 143 | logger.warn(`GitHub fetch error status=${result.status}`); |
| 144 | } |
| 145 | |
| 146 | // Notifier pass: only when we have a known latest, an admin email, and the policy allows notify. |
| 147 | if (state.latest && settings.adminEmail) { |
| 148 | const current = getEpVersion(); |
| 149 | const policy = evaluatePolicy({ |
| 150 | installMethod: detectedMethod, |
| 151 | tier: settings.updates.tier, |
| 152 | current, |
| 153 | latest: state.latest.version, |
| 154 | maintenanceWindow: settings.updates.maintenanceWindow, |
| 155 | }); |
| 156 | if (policy.canNotify) { |
| 157 | const decision = decideEmails({ |
| 158 | adminEmail: settings.adminEmail, |
| 159 | current, |
| 160 | latest: state.latest.version, |
| 161 | latestTag: state.latest.tag, |
| 162 | isSevere: isMinorOrMoreBehind(current, state.latest.version), |
| 163 | state: state.email, |
| 164 | now, |
| 165 | }); |
| 166 | for (const email of decision.toSend) { |
| 167 | await sendEmailViaSmtp(settings.adminEmail, email.subject, email.body); |
| 168 | } |
| 169 | state.email = decision.newState; |
| 170 | } |
no test coverage detected