# Michel Monitor Pull Request Github Actions

> Diagnose a failed, stuck, or never-triggered CI run on a GitHub PR, apply a local fix if possible, push it, and document the result in a single running PR comment. Invoke whenever Michel's CI monitor loop triggers with `any_failure`, `stuck`, or `not_triggered` — the bash loop already handles `pending` and `all_green` silently, so this skill never sees those states.

- Skill: `packmindhub/michel-monitor-pull-request-github-actions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add packmindhub/michel-monitor-pull-request-github-actions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/packmindhub/michel-monitor-pull-request-github-actions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: PackmindHub (https://skillmd.com/u/packmindhub)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/packmindhub/michel-monitor-pull-request-github-actions

---


# Monitor a pull request's GitHub Actions CI

Called during one iteration of the CI monitor loop. The harness prompt already contains everything needed to start:

- `{{CHECKS_JSON}}` — snapshot of all check statuses at loop entry
- `{{REASON}}` — `any_failure`, `stuck`, or `not_triggered`
- `{{PR_NUMBER}}`, `{{BRANCH}}`, `{{WORKDIR}}`, `{{ITER}}`, `{{MAX_ITER}}`

## Decision flow

```
Read CHECKS_JSON in prompt
  → if REASON == "stuck":          document timeout in running comment → done
  → if REASON == "not_triggered":  investigate why CI never started, document → done
  → if REASON == "any_failure":
      1. Identify failing run(s) via gh run list
      2. Fetch failed logs with gh run view --log-failed
      3. Match pattern → apply fix locally
      4. Commit + push
      5. Create or update running comment
      (if unfixable: document reason → done, no push)
```

## 1. Classify — `any_failure` vs `stuck`

The bash outer loop has already classified the state; you are invoked only in these two cases:

| `{{REASON}}`    | Meaning                                                                                            | Action                                                            |
| --------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `any_failure`   | At least one completed check has `conclusion` ∈ `{failure, timed_out, cancelled, action_required}` | Diagnose and fix (sections 2–3)                                   |
| `stuck`         | A workflow **started** but has been in-progress for >20 min                                        | Document timeout in running comment (section 5) — no code changes |
| `not_triggered` | Zero checks **ever** registered for the whole monitoring window — CI never started                 | Investigate why (section 5) and document — no code changes        |

`conclusion` values to recognise: `success` / `failure` / `timed_out` / `cancelled` / `action_required` / `neutral` / `skipped` / `stale` / `null`.

A `completed` check with `conclusion = null` counts as failure.

### gh CLI contract — re-fetch with `gh run list`, NOT `gh pr checks`

Do **not** use `gh pr checks` here. It resolves the commit's `statusCheckRollup`
over GraphQL, and the worker's **fine-grained PAT has no permission that can read
the Checks API** (check-runs). It always partial-fails with `Resource not
accessible by personal access token (…statusCheckRollup.contexts.nodes.N)`,
returns an empty body, and makes a healthy PR look like zero checks — the bug that
produced false `not_triggered` loops. There is no fine-grained permission that
fixes this; only a classic PAT (`repo` scope) or a GitHub App can read check-runs.

Re-fetch CI state with `gh run list` instead — it hits `GET /actions/runs`
(`Actions:read`, which the token has) and reports each workflow run's `status` and
`conclusion` directly, no GraphQL:

```bash
gh run list --branch <BRANCH> --repo <OWNER/REPO> \
  --json databaseId,status,conclusion,workflowName,startedAt,headSha --limit 50
```

Classify only the runs whose `.headSha` equals the PR head commit
(`gh pr view <PR_NUMBER> --repo <OWNER/REPO> --json headRefOid --jq .headRefOid`);
runs on the branch with a different SHA are stale from earlier pushes.

| run state                                                                                            | meaning |
| ---------------------------------------------------------------------------------------------------- | ------- |
| `status != "completed"`                                                                              | pending |
| completed + `conclusion` ∈ `{success, neutral, skipped}`                                             | pass    |
| completed + `conclusion` ∈ `{failure, timed_out, cancelled, action_required, startup_failure, null}` | fail    |

## 2. Diagnose a failure

### Step 1 — Start from the snapshot

Read `{{CHECKS_JSON}}` in the prompt first — it is the snapshot of the head commit's runs (`name`, `status`, `conclusion`, `bucket`) at loop entry. Only re-fetch if you need data fresher than the snapshot, and use `gh run list` to do it (see the gh CLI contract above) — never `gh pr checks`, which the fine-grained PAT cannot read.

### Step 2 — Find the failing run

```bash
gh run list --branch <BRANCH> --repo <OWNER/REPO> \
  --json databaseId,name,status,conclusion,workflowName,url \
  --limit 10 | jq '.[] | select(.conclusion == "failure")'
```

### Step 3 — Read failed step logs

```bash
gh run view <RUN_ID> --repo <OWNER/REPO> --log-failed
```

`--log-failed` returns only the failing steps' output — much smaller than full logs.

### Step 4 — Get job/step names if needed

```bash
gh run view <RUN_ID> --repo <OWNER/REPO> --json jobs
```

### Step 5 — Match the root cause

| Log pattern                | Likely cause                   | Fix                                               |
| -------------------------- | ------------------------------ | ------------------------------------------------- |
| ESLint / Lint errors       | Code violates ESLint rules     | Fix the lint errors in the affected files         |
| Jest / test failure        | Assertion failed or file error | Fix the test or the production code it covers     |
| TypeScript / tsc error     | Type mismatch, bad import      | Fix the type error in the source                  |
| Build error (nx build)     | Missing dep, bad import path   | Fix import or tsconfig                            |
| Missing secret / env var   | Required CI env not set        | Document as unfixable — you cannot add CI secrets |
| Repeated identical failure | Same error as prior iteration  | Document as unfixable — cycling wastes iterations |

**Detecting a repeated failure:** fetch the running comment body, scan for the previous iteration's `**Failure:**` and `**Cause:**` lines, and compare their error signature to the current run's log output. If the signatures match, declare unfixable rather than pushing another identical attempt.

## 3. Apply a fix and push

### Edit and commit

```bash
git -C <WORKDIR> add <changed-files>
git -C <WORKDIR> commit -m "🐛 fix(ci): <short description>"
```

Validate the projects you touched with targeted `nx test <project>` / `nx lint <project>` before pushing, then let CI confirm — CI re-runs the full `nx affected` gate on the PR as the authoritative check (that is what you are monitoring).

### Push

```bash
git push -u origin <BRANCH> --force-with-lease --no-verify
```

`--force-with-lease` only overwrites the remote tip if it matches the last-known ref, so it is safe for force-push. Push with `--no-verify`: the full gate already ran in the validation phase before the first push and CI re-runs it as the authoritative check, so re-running the local pre-push hook here only duplicates CI's work (and a non-zero exit risks losing the run).

## 4. Single running PR comment

There is exactly one running comment per PR, identified by `<!-- michel-ci-monitor -->`. Posting a second comment fragments the history and makes the PR timeline hard to follow — always find the existing one and patch it in place.

### Look up the comment ID

```bash
gh api --paginate "repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments" \
  --jq '[.[] | select(.body | contains("<!-- michel-ci-monitor -->"))] | first | .id'
```

`--paginate` ensures the comment is found even if the PR has more than 30 comments.

### Create the comment (first failure, no comment exists yet)

```bash
gh pr comment <PR_NUMBER> --repo <OWNER>/<REPO> --body "$(cat <<'BODY'
<!-- michel-ci-monitor -->
## Michel — CI Monitor

### Iteration 1

**Failure:** <workflow / job / step>
**Cause:** <1-2 sentences>
**Fix applied:** <what was changed and why>
**Commit:** <git sha>
BODY
)"
```

### Update the comment (subsequent iterations)

Write the new body to a tempfile to avoid shell-quoting issues with `$`, backticks, or multi-line content:

```bash
COMMENT_ID=$(gh api --paginate "repos/<OWNER>/<REPO>/issues/<PR_NUMBER>/comments" \
  --jq '[.[] | select(.body | contains("<!-- michel-ci-monitor -->"))] | first | .id')

EXISTING=$(gh api "repos/<OWNER>/<REPO>/issues/comments/${COMMENT_ID}" --jq '.body')

cat > /tmp/comment-body.md <<BODY
${EXISTING}

### Iteration <N>

**Failure:** <details>
**Cause:** <details>
**Fix applied:** <details or 'Unfixable — see below'>
BODY

gh api -X PATCH "repos/<OWNER>/<REPO>/issues/comments/${COMMENT_ID}" \
  -F body=@/tmp/comment-body.md
```

**Comment body structure** — cumulative, one `### Iteration N` heading per invocation:

- What workflow / job / step failed
- Root cause (from the logs)
- Fix applied — or "Unfixable" with reason
- Git commit SHA if a fix was pushed

## 5. Terminal states — unfixable or timeout

When you cannot fix the problem or the workflow is stuck, update the running comment and return **without committing anything**.

**Unfixable:**

```
### Iteration <N> — Unfixable

**Failure:** <details>
**Reason not fixable:** <missing CI secret | repeated identical failure | infra issue | error outside codebase>
**Recommendation:** Manual intervention required.
```

**Timeout (stuck):**

```
### Timeout reached

Workflow **<name>** has been running for >20 minutes — likely a CI infrastructure issue, not a code problem.
No fix was attempted. Manual re-run or intervention may be needed.
```

**CI did not trigger (not_triggered):**

First confirm with `gh run list --branch <BRANCH> --repo <OWNER/REPO>` whether any
run exists. Zero runs ⇒ the workflow never started — most often no self-hosted
runner was available, a branch/path filter excluded the PR, or the run needs
approval.

```
### CI did not trigger

No checks registered for this PR within the monitoring window. CI did not start
— likely no self-hosted runner available, a branch/path filter, or a required run
approval. No fix was attempted. Verify runner availability / workflow triggers,
then re-run.
```

