Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-false-positive-ready-to-merge-1480.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@link-assistant/hive-mind': patch
---

fix false positive 'Ready to merge' by adding workflow run grace period (Issue #1480)
158 changes: 158 additions & 0 deletions docs/case-studies/issue-1480/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Case Study: Issue #1480 - `Ready to merge` posted as false positive

## Summary

The auto-restart-until-mergeable monitor posted a "Ready to merge" comment on PR #1479
approximately 19 seconds after the last commit was pushed, while CI workflows had not yet
been registered in the GitHub Actions API. CI later started and failed (lint,
check-file-line-limits).

## Timeline

| Time (UTC) | Event |
| ---------- | ----------------------------------------------------------------- |
| 09:53:33 | Last commit `9ed29b7` pushed (Revert "Initial commit") |
| ~09:53:50 | Auto-merge monitor runs `getMergeBlockers()` |
| ~09:53:50 | `getDetailedCIStatus()` returns `no_checks` |
| ~09:53:50 | `checkPRMergeable()` returns `mergeable: true` |
| ~09:53:50 | `getActiveRepoWorkflows()` returns `hasWorkflows: true` |
| ~09:53:50 | `getWorkflowRunsForSha()` returns **0 runs** (not yet registered) |
| ~09:53:50 | Code concludes: "CI was definitively NOT triggered" |
| 09:53:52 | "Ready to merge" comment posted (FALSE POSITIVE) |
| 09:55:18 | CI workflow actually starts running |
| 09:56:11 | CI finishes — `lint` and `check-file-line-limits` FAIL |

## Root Cause

The fix for issue #1442 added `getWorkflowRunsForSha()` to distinguish between:

- Workflow runs triggered but check-runs not yet registered (race condition)
- CI not triggered at all (fork PR, `paths-ignore`, etc.)

The assumption was: if `workflow_runs` API returns 0 runs, CI was "definitively NOT
triggered." But this assumption is flawed — **GitHub Actions workflow runs also take time
to appear in the API after a push** (typically 30-120 seconds).

The race condition timeline:

```
Push → (0-30s) → Workflow runs appear in API → (0-30s) → Check-runs appear in API
```

The code only protected against the second race (workflow runs exist but check-runs don't)
but not the first race (workflow runs themselves don't exist yet).

## GitHub API Data Sources for CI/CD Status

The fix collects data from all available GitHub API endpoints:

### 1. Check Runs API (`GET /repos/{owner}/{repo}/commits/{sha}/check-runs`)

Returns check-runs created by GitHub Actions jobs. These appear **after** a workflow run
starts executing a job. Fields: `status` (queued/in_progress/completed), `conclusion`
(success/failure/cancelled/etc.).

### 2. Commit Statuses API (`GET /repos/{owner}/{repo}/commits/{sha}/status`)

Returns legacy commit statuses (used by third-party CI systems like Jenkins, CircleCI).
Fields: `state` (pending/success/failure/error), `context` (name).

### 3. Workflow Runs API (`GET /repos/{owner}/{repo}/actions/runs?head_sha={sha}`)

Returns GitHub Actions workflow runs triggered for a specific commit SHA. Appears **before**
check-runs but **after** the GitHub Actions scheduler processes the event (30-120s delay).

### 4. Active Workflows API (`GET /repos/{owner}/{repo}/actions/workflows`)

Lists all workflows configured in the repo with their state (active/disabled). Filtered
to exclude GitHub Pages deployment workflows which only run on default branch.

### 5. Repository Content API (`GET /repos/{owner}/{repo}/contents/.github/workflows`)

Parses actual workflow YAML files to detect PR-related triggers (`on: pull_request`,
`on: push`, `on: pull_request_target`). Provides ground-truth about what CI **should** run.

### 6. PR Commits API (`GET /repos/{owner}/{repo}/pulls/{number}/commits`)

Lists all commits in a PR. Used to check if previous commits had CI workflow runs,
which is a signal that the HEAD commit should also have CI.

### 7. Commit API (`GET /repos/{owner}/{repo}/commits/{sha}`)

Returns commit metadata including `commit.committer.date`, used to calculate commit age
for the grace period check.

## Fix: Multi-Layer Defense

Instead of immediately concluding "CI not triggered" when `getWorkflowRunsForSha()` returns
0 runs, the code now uses a multi-layer defense:

### Layer 1: Empty Workflows Folder Detection

If `.github/workflows/` doesn't exist or has no `.yml`/`.yaml` files, immediately conclude
"no CI configured at file level" — no grace period needed.

### Layer 2: Grace Period (120 seconds)

Check the age of the HEAD commit. If pushed within the last 120 seconds, treat as a
potential race condition and return `ci_pending` blocker. Additionally report workflow
file PR triggers as context.

### Layer 3: Previous Commit CI History

After grace period elapses, check if earlier commits in the same PR had workflow runs.
If previous commits had CI AND workflow files have PR triggers, it's likely a GitHub API
delay — wait one more cycle as safety measure.

### Layer 4: Definitive Conclusion

Only conclude "CI not triggered" when ALL conditions are met:

- Grace period (120s) has elapsed
- No workflow runs for HEAD SHA
- Previous PR commits did NOT have CI, OR workflow files don't have PR triggers

## Code Path (after fix)

```
getMergeBlockers() → src/solve.auto-merge.lib.mjs:190
├── getDetailedCIStatus() → returns no_checks
├── checkPRMergeable() → returns mergeable: true
├── getActiveRepoWorkflows() → returns hasWorkflows: true
├── getWorkflowRunsForSha() → returns [] (0 runs for HEAD SHA)
│ ├── checkWorkflowsHavePRTriggers() → parse .github/workflows/*.yml
│ │ ├── hasWorkflowFiles: false → immediate "no CI" conclusion
│ │ └── hasWorkflowFiles: true →
│ ├── getCommitDate() → check commit age
│ │ ├── < 120s → ci_pending blocker (Layer 2: grace period)
│ │ └── >= 120s →
│ │ ├── checkPreviousPRCommitsHadCI() → check earlier commits
│ │ │ ├── had CI + has PR triggers → ci_pending (Layer 3: safety)
│ │ │ └── no previous CI or no triggers → noCiTriggered: true
│ │ └── (Layer 4: definitive conclusion)
```

## Related Issues

- Issue #1442: No CI timeout handling (introduced the flawed `getWorkflowRunsForSha` check)
- Issue #1466: Auto-restart stuck with `action_required` workflows
- Issue #1363: False positive "ready to merge" for repos with no required checks
- Issue #1345: Differentiate "no CI configured" vs "CI not triggered"
- Issue #1425: False positive "failed CI" detection

## Evidence

- False positive comment: https://github.com/link-assistant/hive-mind/pull/1479#issuecomment-4125186525
- CI run showing failures: PR #1479 statusCheckRollup shows lint FAILURE at 09:56:11
- Solution draft log: https://github.com/link-assistant/hive-mind/pull/1479#issuecomment-4125185439
- PR #1479 details with full CI timeline: `data/pr-1479-details.json`
- PR #1479 comments: `data/pr-1479-all-comments.txt`

## GitHub API Documentation References

- [Check Runs API](https://docs.github.com/en/rest/checks/runs)
- [Commit Statuses API](https://docs.github.com/en/rest/commits/statuses)
- [Workflow Runs API](https://docs.github.com/en/rest/actions/workflow-runs)
- [List Repository Workflows](https://docs.github.com/en/rest/actions/workflows)
- [Get Repository Content](https://docs.github.com/en/rest/repos/contents)
- [List PR Commits](https://docs.github.com/en/rest/pulls/pulls#list-commits-on-a-pull-request)
41 changes: 41 additions & 0 deletions docs/case-studies/issue-1480/data/pr-1479-all-comments.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
author: konard
association: member
edited: false
status: none
--
## 🤖 Solution Draft Log
This log file contains the complete execution trace of the AI solution draft process.

### 💰 **Cost estimation:**
- Public pricing estimate: $3.537043
- Calculated by Anthropic: $2.980201 USD
- Difference: $-0.556842 (-15.74%)

### 🤖 **Models used:**
- Tool: Anthropic Claude Code
- Requested: `opus`
- **Main model: Claude Opus 4.6** (`claude-opus-4-6`)
- **Additional models:**
* **Claude Haiku 4.5** (`claude-haiku-4-5-20251001`)

### 📎 **Log file uploaded as Gist** (1592KB)
- [View complete solution draft log](https://gist.githubusercontent.com/konard/952725c5f9962321f19afeb714c83d0e/raw/93577632c068860a7bae2eeae4fea85f39bd61dd/solution-draft-log-pr-1774432416684.txt)

---
*Now working session is ended, feel free to review and add any feedback on the solution draft.*
--
author: konard
association: member
edited: false
status: none
--
## ✅ Ready to merge

This pull request is now ready to be merged:
- CI workflows exist but were not triggered for this commit
- No merge conflicts
- No pending changes

---
*Monitored by hive-mind with --auto-restart-until-mergeable flag*
--
1 change: 1 addition & 0 deletions docs/case-studies/issue-1480/data/pr-1479-ci-runs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
30 changes: 30 additions & 0 deletions docs/case-studies/issue-1480/data/pr-1479-comments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"url": "https://api.github.com/repos/link-assistant/hive-mind/issues/comments/4125185439",
"html_url": "https://github.com/link-assistant/hive-mind/pull/1479#issuecomment-4125185439",
"issue_url": "https://api.github.com/repos/link-assistant/hive-mind/issues/1479",
"id": 4125185439,
"node_id": "IC_kwDOPUU0qc714VWf",
"user": { "login": "konard", "id": 1431904, "node_id": "MDQ6VXNlcjE0MzE5MDQ=", "avatar_url": "https://avatars.githubusercontent.com/u/1431904?v=4", "gravatar_id": "", "url": "https://api.github.com/users/konard", "html_url": "https://github.com/konard", "followers_url": "https://api.github.com/users/konard/followers", "following_url": "https://api.github.com/users/konard/following{/other_user}", "gists_url": "https://api.github.com/users/konard/gists{/gist_id}", "starred_url": "https://api.github.com/users/konard/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/konard/subscriptions", "organizations_url": "https://api.github.com/users/konard/orgs", "repos_url": "https://api.github.com/users/konard/repos", "events_url": "https://api.github.com/users/konard/events{/privacy}", "received_events_url": "https://api.github.com/users/konard/received_events", "type": "User", "user_view_type": "public", "site_admin": false },
"created_at": "2026-03-25T09:53:41Z",
"updated_at": "2026-03-25T09:53:41Z",
"body": "## 🤖 Solution Draft Log\nThis log file contains the complete execution trace of the AI solution draft process.\n\n### 💰 **Cost estimation:**\n- Public pricing estimate: $3.537043\n- Calculated by Anthropic: $2.980201 USD\n- Difference: $-0.556842 (-15.74%)\n\n### 🤖 **Models used:**\n- Tool: Anthropic Claude Code\n- Requested: `opus`\n- **Main model: Claude Opus 4.6** (`claude-opus-4-6`)\n- **Additional models:**\n * **Claude Haiku 4.5** (`claude-haiku-4-5-20251001`)\n\n### 📎 **Log file uploaded as Gist** (1592KB)\n- [View complete solution draft log](https://gist.githubusercontent.com/konard/952725c5f9962321f19afeb714c83d0e/raw/93577632c068860a7bae2eeae4fea85f39bd61dd/solution-draft-log-pr-1774432416684.txt)\n\n---\n*Now working session is ended, feel free to review and add any feedback on the solution draft.*",
"author_association": "MEMBER",
"reactions": { "url": "https://api.github.com/repos/link-assistant/hive-mind/issues/comments/4125185439/reactions", "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 },
"performed_via_github_app": null
},
{
"url": "https://api.github.com/repos/link-assistant/hive-mind/issues/comments/4125186525",
"html_url": "https://github.com/link-assistant/hive-mind/pull/1479#issuecomment-4125186525",
"issue_url": "https://api.github.com/repos/link-assistant/hive-mind/issues/1479",
"id": 4125186525,
"node_id": "IC_kwDOPUU0qc714Vnd",
"user": { "login": "konard", "id": 1431904, "node_id": "MDQ6VXNlcjE0MzE5MDQ=", "avatar_url": "https://avatars.githubusercontent.com/u/1431904?v=4", "gravatar_id": "", "url": "https://api.github.com/users/konard", "html_url": "https://github.com/konard", "followers_url": "https://api.github.com/users/konard/followers", "following_url": "https://api.github.com/users/konard/following{/other_user}", "gists_url": "https://api.github.com/users/konard/gists{/gist_id}", "starred_url": "https://api.github.com/users/konard/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/konard/subscriptions", "organizations_url": "https://api.github.com/users/konard/orgs", "repos_url": "https://api.github.com/users/konard/repos", "events_url": "https://api.github.com/users/konard/events{/privacy}", "received_events_url": "https://api.github.com/users/konard/received_events", "type": "User", "user_view_type": "public", "site_admin": false },
"created_at": "2026-03-25T09:53:52Z",
"updated_at": "2026-03-25T09:53:52Z",
"body": "## ✅ Ready to merge\n\nThis pull request is now ready to be merged:\n- CI workflows exist but were not triggered for this commit\n- No merge conflicts\n- No pending changes\n\n---\n*Monitored by hive-mind with --auto-restart-until-mergeable flag*",
"author_association": "MEMBER",
"reactions": { "url": "https://api.github.com/repos/link-assistant/hive-mind/issues/comments/4125186525/reactions", "total_count": 0, "+1": 0, "-1": 0, "laugh": 0, "hooray": 0, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 },
"performed_via_github_app": null
}
]
Loading