Reported symptom
"why do I get so many notifications that PAGS is live?"
That string is lib/deploy-watch.ts:90 — `${repoName} is live.` — the body of the ✅ deploy
notification. The user is getting several per push, and some ❌ failure notifications for builds that
did not fail.
Cause: the watcher asks GitHub for "the newest run" and this repo has seven workflows
runDeployWatch polls one endpoint per repo (lib/deploy-watch.ts:133):
const res = await fetchWorkflowRuns(repo.github_repo, token, { perPage: 1 });
const raw = "runs" in res ? res.runs[0] : undefined;
and fetchWorkflowRuns (lib/github-actions.ts:28-44) builds its query from per_page and page
only:
const qs = `per_page=${perPage}${page > 1 ? `&page=${page}` : ""}`;
fetch(`https://github.com/ghapi/repos/${repo}/actions/runs?${qs}`)
No workflow_id, no branch, no event, no status. So runs[0] is the newest run across every
workflow in the repo. ProAgentStore/platform has seven:
ci.yml deploy-api.yml deploy-host.yml deploy-mcp.yml
deploy-testflight.yml publish-mcp-registry.yml publish-npm.yml
One push to main starts two to four of them. They finish at different times, so the per-minute cron
sees a different newest run id on successive sweeps, and decideDeployNotification fires on each
one — its only dedup is lastNotifiedRunId === run.id (:79), a single watermark column
(coding_repos.last_deploy_run_id) against an interleaved stream of runs from unrelated workflows.
It can never settle.
Real runs from this repo, same push, two workflows each:
b1266cf CI success
b1266cf Deploy API Wor… success
31e41cc CI success
31e41cc Deploy Host Wo… success
≈2–4 notifications per push. The session before this filed ~20 pushes in three hours.
Three separate wrongnesses, not one
1. CI is not a deploy. A green ci.yml run produces "✅ Deployed #412 — PAGS is live." Nothing
was deployed and nothing went live; typecheck passed. This is the notification claiming an outcome it
has no evidence for — the same honesty rule the platform applies to agents.
2. A cancelled run reports as a build failure. ok = run.conclusion === "success" (:84), so
anything else takes the ❌ branch: "❌ Build failed #N — PAGS — cancelled." Superseded runs are
cancelled routinely by concurrency groups (there are two in the recent list), so pushing twice in
quick succession sends a failure alarm for a build that was merely replaced.
3. The run number is per-workflow. #412 from CI and #88 from Deploy API arrive minutes apart
for the same commit, so the number carries no usable meaning across notifications.
No dedup anywhere downstream
notifyUser (routes/push.ts:196-206) does two things with no coalescing, no rate limit and no
recent-duplicate check:
createNotification(...) — the in-app list keeps every copy;
sendPushToUser(..., { tag: type }) — tag is the literal string "deploy", so the OS tray
visually collapses them to one, but each still fires its alert. That is why it feels like far more
notifications than the tray shows: the buzzes are real, the tray is lying by collapsing them.
Fix
The root error is treating any completed workflow run as a deploy that put something live. The
watcher needs to know which workflow is the deploy, and only speak for that one.
Options, in rough order of preference:
- Filter the query.
fetchWorkflowRuns should accept branch, event and status, and the
watcher should pass the repo's default branch, event=push, status=completed. That alone kills
the PR/branch noise and the in-progress churn, but not the CI-vs-deploy confusion, because CI
also runs on push to main.
- Name the deploy workflow. Either let a
coding_repos row declare it, or default to matching
the workflow file/name against a deploy pattern, and query .../actions/workflows/<id>/runs. This
is what actually fixes it — one workflow, one watermark, one notification.
- If more than one workflow legitimately deploys (this repo deploys api, host and mcp
separately), the watermark has to become per (repo, workflow) rather than the single
last_deploy_run_id column, and the body should name which one — "api Worker is live", not
"PAGS is live".
Whatever is chosen, conclusion === "cancelled" should be silent, not a failure alarm.
Worth considering alongside
notifyUser has no duplicate suppression at all, and this is the second notification bug in the same
area (#338, #344, #349 were all deploy/handoff notification defects). A cheap floor — drop an
identical (user, type, title, body) inside some short window — would have bounded this one to a single
buzz instead of eighty, regardless of the watcher's logic.
Reported symptom
That string is
lib/deploy-watch.ts:90—`${repoName} is live.`— the body of the ✅ deploynotification. The user is getting several per push, and some ❌ failure notifications for builds that
did not fail.
Cause: the watcher asks GitHub for "the newest run" and this repo has seven workflows
runDeployWatchpolls one endpoint per repo (lib/deploy-watch.ts:133):and
fetchWorkflowRuns(lib/github-actions.ts:28-44) builds its query fromper_pageandpageonly:
No
workflow_id, nobranch, noevent, nostatus. Soruns[0]is the newest run across everyworkflow in the repo.
ProAgentStore/platformhas seven:One push to
mainstarts two to four of them. They finish at different times, so the per-minute cronsees a different newest run id on successive sweeps, and
decideDeployNotificationfires on eachone — its only dedup is
lastNotifiedRunId === run.id(:79), a single watermark column(
coding_repos.last_deploy_run_id) against an interleaved stream of runs from unrelated workflows.It can never settle.
Real runs from this repo, same push, two workflows each:
≈2–4 notifications per push. The session before this filed ~20 pushes in three hours.
Three separate wrongnesses, not one
1. CI is not a deploy. A green
ci.ymlrun produces "✅ Deployed #412 — PAGS is live." Nothingwas deployed and nothing went live; typecheck passed. This is the notification claiming an outcome it
has no evidence for — the same honesty rule the platform applies to agents.
2. A cancelled run reports as a build failure.
ok = run.conclusion === "success"(:84), soanything else takes the ❌ branch: "❌ Build failed #N — PAGS — cancelled." Superseded runs are
cancelled routinely by concurrency groups (there are two in the recent list), so pushing twice in
quick succession sends a failure alarm for a build that was merely replaced.
3. The run number is per-workflow.
#412from CI and#88from Deploy API arrive minutes apartfor the same commit, so the number carries no usable meaning across notifications.
No dedup anywhere downstream
notifyUser(routes/push.ts:196-206) does two things with no coalescing, no rate limit and norecent-duplicate check:
createNotification(...)— the in-app list keeps every copy;sendPushToUser(..., { tag: type })—tagis the literal string"deploy", so the OS trayvisually collapses them to one, but each still fires its alert. That is why it feels like far more
notifications than the tray shows: the buzzes are real, the tray is lying by collapsing them.
Fix
The root error is treating any completed workflow run as a deploy that put something live. The
watcher needs to know which workflow is the deploy, and only speak for that one.
Options, in rough order of preference:
fetchWorkflowRunsshould acceptbranch,eventandstatus, and thewatcher should pass the repo's default branch,
event=push,status=completed. That alone killsthe PR/branch noise and the in-progress churn, but not the CI-vs-deploy confusion, because CI
also runs on push to main.
coding_reposrow declare it, or default to matchingthe workflow file/name against a deploy pattern, and query
.../actions/workflows/<id>/runs. Thisis what actually fixes it — one workflow, one watermark, one notification.
separately), the watermark has to become per (repo, workflow) rather than the single
last_deploy_run_idcolumn, and the body should name which one — "api Worker is live", not"PAGS is live".
Whatever is chosen,
conclusion === "cancelled"should be silent, not a failure alarm.Worth considering alongside
notifyUserhas no duplicate suppression at all, and this is the second notification bug in the samearea (#338, #344, #349 were all deploy/handoff notification defects). A cheap floor — drop an
identical (user, type, title, body) inside some short window — would have bounded this one to a single
buzz instead of eighty, regardless of the watcher's logic.