function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, dialog = null, statusNode = null, checksNode = null, releaseNode = null, listNode = null, documentRef = null, windowRef = null, setTimer = setTimeout, clearTimer = clearTimeout, pollMs = 30000 }) { const prefix = 'stackchain.release-receipt.v1:'; const limit = 12; let entries = []; let refreshing = null; let timer = null; let bound = false; documentRef ||= typeof document !== 'undefined' ? document : null; windowRef ||= typeof window !== 'undefined' ? window : null; const login = () => String(getLogin?.() || '').trim().toLowerCase(); const key = () => prefix + login(); const identity = value => String(value.repository || '') + '@' + String(value.commit_sha || ''); const path = value => 'api/v1/repos/' + String(value.repository || '').split('/') .map(encodeURIComponent).join('/') + '/release-receipt/' + encodeURIComponent(value.commit_sha); function valid(value, account) { return value && value.account === account && value.repository && value.commit_sha; } function persist() { if (!entries.length) storage?.removeItem(key()); else storage?.setItem(key(), JSON.stringify({ version: 2, account: login(), entries })); } const hasPending = () => entries.some(entry => !entry.status?.release && entry.status?.label !== 'Checks failed'); function schedule(delay = pollMs) { if (!bound || documentRef?.hidden || !hasPending()) return; if (timer !== null) clearTimer(timer); timer = setTimer(async () => { timer = null; try { await refresh(); } catch (error) { if (statusNode) statusNode.textContent = error.message + ' Retry when connected.'; } schedule(); }, delay); } function restore() { const account = login(); if (!account) return []; try { const parsed = JSON.parse(storage?.getItem(prefix + account) || 'null'); const stored = parsed?.version === 2 && parsed.account === account ? parsed.entries : (valid(parsed, account) ? [parsed] : []); entries = Array.isArray(stored) ? stored.filter(value => valid(value, account)).slice(-limit) : []; if (entries.length && parsed?.version !== 2) persist(); } catch (_error) { entries = []; } render(); schedule(); return entries.map(value => ({ ...value })); } function capture(item, mergeResult) { const account = login(); const commitSha = String(mergeResult?.merge_commit_sha || '').trim(); if (!account || !item?.repository || !commitSha) throw new Error('The exact merge commit is unavailable.'); if (!entries.length) restore(); const entry = { account, repository: item.repository, number: Number(item.number), key: item.key || item.repository + '#' + item.number, commit_sha: commitSha, status: null, captured_at: new Date().toISOString(), }; const existing = entries.findIndex(value => identity(value) === identity(entry)); if (existing >= 0) { entry.status = entries[existing].status || null; entries.splice(existing, 1, entry); } else { entries.push(entry); entries = entries.slice(-limit); } persist(); render(); schedule(); return { ...entry }; } function summarize(payload) { const checks = Array.isArray(payload?.checks) ? payload.checks : []; const failing = checks.filter(check => ['failure', 'error'].includes(check.state)).map(check => check.name).filter(Boolean); const pending = checks.filter(check => check.state === 'pending').map(check => check.name).filter(Boolean); if (payload?.release) return { ...payload, label: 'Released · ' + payload.release.tag, checks: [] }; if (failing.length) return { ...payload, label: 'Checks failed', checks: failing }; if (payload?.ci_state === 'success') return { ...payload, label: 'Checks passed · waiting for release', checks: [] }; return { ...payload, label: 'Checks running', checks: pending }; } function render() { const visible = entries.map((entry, index) => ({ entry, index })) .sort((a, b) => Number(b.entry.status?.label === 'Checks failed') - Number(a.entry.status?.label === 'Checks failed') || a.index - b.index) .map(value => value.entry); const first = visible[0] || null; const failed = visible.filter(entry => entry.status?.label === 'Checks failed').length; if (launcher) { launcher.hidden = !entries.length; launcher.textContent = failed ? failed + ' release ' + (failed === 1 ? 'failure' : 'failures') + ' · ' + visible.length + ' tracked' : visible.length + ' ' + (visible.length === 1 ? 'merge' : 'merges') + ' · tracking release'; } if (statusNode) statusNode.textContent = first?.status?.label || 'Checking the exact merge commit…'; if (checksNode) checksNode.textContent = (first?.status?.checks || []).join(', '); if (releaseNode) { releaseNode.hidden = !first?.status?.release?.url; if (first?.status?.release?.url) { releaseNode.href = first.status.release.url; releaseNode.textContent = 'Open release ' + first.status.release.tag; } } if (listNode) { const rows = visible.map(entry => { const row = document.createElement('article'); row.className = 'release-watchlist-item'; const copy = document.createElement('div'); const title = document.createElement('strong'); title.textContent = entry.key; const state = document.createElement('span'); state.className = 'small'; state.textContent = entry.status?.label || 'Checking the exact merge commit…'; copy.append(title, state); if (entry.status?.release?.url) { const link = document.createElement('a'); link.href = entry.status.release.url; link.textContent = 'Open release ' + entry.status.release.tag; copy.append(link); } const button = document.createElement('button'); button.type = 'button'; button.textContent = 'Dismiss'; button.setAttribute('aria-label', 'Dismiss ' + entry.key + ' from release tracking'); button.addEventListener('click', () => dismiss(entry.repository, entry.commit_sha)); row.append(copy, button); return row; }); listNode.replaceChildren(...rows); } } async function refreshEntry(entry) { const payload = await fetchJson(path(entry), { headers: { Accept: 'application/json' } }); if (payload?.commit_sha !== entry.commit_sha) throw new Error('Release evidence did not match the merged commit.'); entry.status = summarize(payload); return entry.status; } async function refresh() { if (refreshing) return refreshing; if (!entries.length) restore(); if (!entries.length) return []; refreshing = (async () => { const statuses = []; for (const entry of entries) { try { statuses.push(await refreshEntry(entry)); } catch (error) { entry.status = { label: 'Status unavailable', checks: [], error: String(error?.message || error) }; statuses.push(entry.status); } } persist(); render(); return statuses; })(); try { return await refreshing; } finally { refreshing = null; } } function dismiss(repository, commitSha) { if (repository && commitSha) entries = entries.filter(entry => identity(entry) !== repository + '@' + commitSha); else entries = []; persist(); render(); schedule(); if (!entries.length && dialog?.open) dialog.close(); return entries.map(value => ({ ...value })); } function bind() { bound = true; launcher?.addEventListener('click', async () => { dialog?.showModal?.(); try { await refresh(); } catch (error) { if (statusNode) statusNode.textContent = error.message + ' Retry when connected.'; } }); documentRef?.addEventListener('visibilitychange', () => { if (documentRef.hidden) { if (timer !== null) clearTimer(timer); timer = null; } else schedule(0); }); windowRef?.addEventListener('online', () => schedule(0)); schedule(); } return { capture, restore, refresh, dismiss, bind, fetchJson }; } if (typeof module !== 'undefined' && module.exports) module.exports = createReleaseReceipt;