feat: track exact post-merge release progress (Closes #1092)
CI / lint (pull_request) Successful in 3m13s
CI / build-release (pull_request) Successful in 9s
CI / browser-journey (pull_request) Successful in 3m28s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-18 19:41:00 +00:00
1 parent 087300f9c3
commit 6db0802461
11 files changed
+374 -3

No files matched your search

+8
View File
@@ -14,6 +14,14 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
.live-data-status-header button, .live-data-status-actions button { min-height:44px; }
.issue-filing-receipt { position:fixed; inset:0; z-index:108; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
.issue-filing-receipt[hidden] { display:none; }
.release-receipt-sheet { position:fixed; inset:0; width:100%; max-width:none; height:100%; max-height:none; margin:0; padding:0; border:0; background:rgba(5,12,21,.82); color:#e5e7eb; }
.release-receipt-sheet::backdrop { background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
.release-receipt-panel { position:absolute; left:0; right:0; bottom:0; box-sizing:border-box; width:min(560px,100%); max-height:100dvh; margin:auto; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
.release-receipt-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
.release-receipt-launcher { width:100%; min-height:44px; border-color:#4ade80; }
.release-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
.release-receipt-panel button, .release-receipt-panel .button-link { box-sizing:border-box; display:flex; align-items:center; justify-content:center; min-width:0; min-height:44px; width:100%; text-align:center; }
@media (max-width:359px) { .release-receipt-actions { grid-template-columns:1fr; } }
.issue-filing-receipt-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
.issue-filing-receipt-panel h2, .issue-filing-receipt-panel h3 { margin:.25rem 0; }
.issue-filing-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
+30 -1
View File
@@ -334,6 +334,23 @@
let confirmedOwnerLogin = '';
let planningOwnerLogin = '';
let activeFlushLogin = '';
let releaseReceipt = null;
function attachReleaseReceipt() {
if (releaseReceipt) return releaseReceipt;
releaseReceipt = createReleaseReceipt({
storage:localStorage, getLogin:()=>confirmedOwnerLogin, fetchJson:fetchReviewJson,
launcher:qs('#release-receipt-launcher'), dialog:qs('#release-receipt-sheet'),
statusNode:qs('#release-receipt-status'), checksNode:qs('#release-receipt-checks'),
releaseNode:qs('#release-receipt-link'),
});
releaseReceipt.bind();
qs('#close-release-receipt').addEventListener('click', () => qs('#release-receipt-sheet').close());
qs('#refresh-release-receipt').addEventListener('click', () => releaseReceipt.refresh().catch(error => {
qs('#release-receipt-status').textContent = error.message + ' Retry when connected.';
}));
qs('#dismiss-release-receipt').addEventListener('click', () => releaseReceipt.dismiss());
return releaseReceipt;
}
const completedFiledReview = createCompletedFiledReview({
storage: localStorage,
getLogin() { return planningOwnerLogin; },
@@ -1136,6 +1153,7 @@
return false;
});
}
attachReleaseReceipt();
if (!reviewController) reviewController = createReviewController({ fetchJson: fetchReviewJson, storage: localStorage });
if (!wrapPreference) {
wrapPreference = createReviewController.createWrapPreference({
@@ -1145,6 +1163,14 @@
}
});
}
function restoreReleaseReceipt() {
const account = String(confirmedOwnerLogin || '').trim().toLowerCase();
if (!account) return;
try {
if (!localStorage.getItem('stackchain.release-receipt.v1:' + account)) return;
} catch (_error) { return; }
ensurePullWorkflow().then(() => releaseReceipt.restore()).catch(() => {});
}
const draftInbox = createDraftInbox({ storage: localStorage, getCurrentLogin: () => activeFlushLogin });
outboxCoordinator.subscribe(() => refreshMyWorkView());
const offlineWorkStore = createOfflineWorkStore({ storage: localStorage, indexedDB:window.indexedDB });
@@ -5246,6 +5272,7 @@
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
if (activeFlushLogin) {
confirmedOwnerLogin = activeFlushLogin;
restoreReleaseReceipt();
updateDeliveryReceiptControls();
interruptionPrompt.restore();
}
@@ -7019,7 +7046,8 @@
button.disabled = true;
qs('#pull-sheet-status').textContent = 'Merging pull request…';
try {
await pullController.merge(selectedPull, selectedPullDetail.head_sha);
const mergeResult = await pullController.merge(selectedPull, selectedPullDetail.head_sha);
releaseReceipt.capture(merging, mergeResult);
closePullSheet();
lastMyWork = lastMyWork.filter(item =>
!(item.kind === 'pull' && item.repository === merging.repository && item.number === merging.number)
@@ -7493,6 +7521,7 @@
if (!saved) return false;
const outage = mode === 'outage';
confirmedOwnerLogin = String(saved.user?.login || '').trim();
restoreReleaseReceipt();
planningOwnerLogin = confirmedOwnerLogin;
interruptionPrompt.restore();
updatePlanningAvailability();
+11
View File
@@ -302,6 +302,7 @@
<div class="small" id="notification-page-status" aria-live="polite"></div>
<button class="load-more-notifications" id="load-more-notifications" type="button" hidden>Load older updates</button>
<div class="small" id="my-work-action-status" aria-live="assertive"></div>
<button class="release-receipt-launcher" id="release-receipt-launcher" type="button" hidden>Merged · tracking release</button>
<button class="retry-work-route" id="retry-unfiled-draft-sync" type="button" hidden>Retry Draft sync</button>
<div class="small" id="today-sync-status" aria-live="polite">Today is saved on this device.</div>
<div class="small" id="today-session-sync-status" role="status" aria-live="polite" hidden></div>
@@ -319,6 +320,15 @@
</div>
</div>
</section>
<dialog class="release-receipt-sheet" id="release-receipt-sheet" aria-labelledby="release-receipt-title">
<section class="release-receipt-panel">
<header><div><p class="small muted">Exact merge evidence</p><h2 id="release-receipt-title">Release progress</h2></div><button id="close-release-receipt" type="button">Close</button></header>
<p id="release-receipt-status" role="status" aria-live="polite">Checking the exact merge commit…</p>
<p class="small" id="release-receipt-checks"></p>
<a class="button-link" id="release-receipt-link" href="" hidden>Open release</a>
<div class="release-receipt-actions"><button id="refresh-release-receipt" type="button">Refresh</button><button id="dismiss-release-receipt" type="button">Dismiss</button></div>
</section>
</dialog>
<dialog class="agenda-export-sheet" id="agenda-export-sheet" aria-labelledby="agenda-export-title">
<section class="agenda-export-panel">
<header>
@@ -1875,6 +1885,7 @@
<script src="static/queue-today.js"></script>
<script src="static/pull-sheet.js"></script>
<script src="static/review-sheet.js"></script>
<script src="static/release-receipt.js"></script>
<script src="static/work-route.js"></script>
<script src="static/task-overlay-history.js"></script>
<script src="static/context-poller.js"></script>
+95
View File
@@ -0,0 +1,95 @@
function createReleaseReceipt({ storage, getLogin, fetchJson, launcher = null, dialog = null, statusNode = null, checksNode = null, releaseNode = null }) {
const prefix = 'stackchain.release-receipt.v1:';
let receipt = null;
const login = () => String(getLogin?.() || '').trim().toLowerCase();
const key = () => prefix + login();
const path = value => 'api/v1/repos/' + String(value.repository || '').split('/')
.map(encodeURIComponent).join('/') + '/release-receipt/' + encodeURIComponent(value.commit_sha);
function restore() {
const account = login();
if (!account) return null;
try {
const parsed = JSON.parse(storage?.getItem(prefix + account) || 'null');
receipt = parsed && parsed.account === account && parsed.repository && parsed.commit_sha ? parsed : null;
} catch (_error) { receipt = null; }
render(receipt?.status || null);
return receipt;
}
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.');
receipt = {
account,
repository: item.repository,
number: Number(item.number),
key: item.key || item.repository + '#' + item.number,
commit_sha: commitSha,
status: null,
};
storage?.setItem(key(), JSON.stringify(receipt));
render(null);
return receipt;
}
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(status) {
if (launcher) {
launcher.hidden = !receipt;
launcher.textContent = status?.label || (receipt ? 'Merged · tracking release' : '');
}
if (statusNode) statusNode.textContent = status?.label || 'Checking the exact merge commit…';
if (checksNode) checksNode.textContent = (status?.checks || []).join(', ');
if (releaseNode) {
releaseNode.hidden = !status?.release?.url;
if (status?.release?.url) {
releaseNode.href = status.release.url;
releaseNode.textContent = 'Open release ' + status.release.tag;
}
}
}
async function refresh() {
if (!receipt) restore();
if (!receipt) return null;
const payload = await fetchJson(path(receipt), { headers: { Accept: 'application/json' } });
if (payload?.commit_sha !== receipt.commit_sha) throw new Error('Release evidence did not match the merged commit.');
const status = summarize(payload);
receipt.status = status;
storage?.setItem(key(), JSON.stringify(receipt));
render(status);
return status;
}
function dismiss() {
const accountKey = key();
storage?.removeItem(accountKey);
receipt = null;
render(null);
if (dialog?.open) dialog.close();
}
function bind() {
launcher?.addEventListener('click', async () => {
dialog?.showModal?.();
try { await refresh(); }
catch (error) { if (statusNode) statusNode.textContent = error.message + ' Retry when connected.'; }
});
}
return { capture, restore, refresh, dismiss, bind, fetchJson };
}
if (typeof module !== 'undefined' && module.exports) module.exports = createReleaseReceipt;
+1
View File
@@ -105,6 +105,7 @@ const SHELL = [
BASE + 'static/queue-today.js',
BASE + 'static/pull-sheet.js',
BASE + 'static/review-sheet.js',
BASE + 'static/release-receipt.js',
BASE + 'static/work-route.js',
BASE + 'static/task-overlay-history.js',
BASE + 'static/context-poller.js',