|
| 1 | +# Allow Apply When Only `digger/apply` Blocks Mergeability |
| 2 | + |
| 3 | +- Related issues: #1150, #959 |
| 4 | +- Areas: `libs/ci/github`, `cli/pkg/digger`, `backend/controllers`, `docs` |
| 5 | + |
| 6 | +## Goal |
| 7 | +Unblock `digger apply` when GitHub reports PR mergeability state as "blocked" but the only blocking status checks are Digger’s own aggregate/per‑project apply checks (`digger/apply` and `*/apply`). |
| 8 | + |
| 9 | +## Problem / Context |
| 10 | +- We gate apply on mergeability: `IsMergeable()` must be true, otherwise `digger apply` errors: "cannot perform Apply since the PR is not currently mergeable". |
| 11 | +- When `digger/apply` is configured as a required status, GitHub reports the PR as `mergeable=false` with `mergeable_state=blocked` until apply completes. |
| 12 | +- This creates a circular dependency: apply is needed to make the PR mergeable, but apply refuses to run because the PR is not mergeable. |
| 13 | + |
| 14 | +Current code (GitHub): |
| 15 | +- `IsMergeable(prNumber)` returns `pr.GetMergeable() && isMergeableState(pr.GetMergeableState())` where allowed states are `clean`, `unstable`, `has_hooks`. |
| 16 | +- Apply gate uses this in two places: |
| 17 | + - `cli/pkg/digger/digger.go` before running apply. |
| 18 | + - `libs/apply_requirements.CheckApplyRequirements` when `apply_requirements` includes `mergeable` (default). |
| 19 | + |
| 20 | +## Non-Goals |
| 21 | +- Changing auto-merge behavior after successful applies (we already require combined status `success`). |
| 22 | +- Implementing Checks API inspection (we only need to introspect commit statuses we set today). |
| 23 | +- Changing mergeability logic for non-GitHub providers. |
| 24 | + |
| 25 | +## Approach |
| 26 | +Augment GitHub mergeability logic to handle the chicken‑and‑egg case: |
| 27 | + |
| 28 | +1) Keep existing fast path: if REST `mergeable==true` and `mergeable_state` in {`clean`, `unstable`, `has_hooks`}, return true. |
| 29 | +2) If `mergeable_state == "blocked"`, fetch combined commit statuses for the PR head SHA. |
| 30 | +3) If every non‑success context is either: |
| 31 | + - `digger/apply` (aggregate), or |
| 32 | + - per‑project apply contexts matching `*/apply` (e.g., `projectA/apply`), |
| 33 | + then treat the PR as mergeable for the purpose of running apply. Otherwise, return false. |
| 34 | +4) Log diagnostics: mergeable bool, mergeable_state, and the list of non‑success contexts checked. |
| 35 | + |
| 36 | +Notes |
| 37 | +- Reviews/draft can also yield `blocked` but won’t appear in commit statuses. If users want to require approvals, they should include `approved` in `apply_requirements` (separate gate already exists). We deliberately do not block here based on reviews unless configured via `apply_requirements`. |
| 38 | +- We continue to respect `skip_merge_check` and project-level `apply_requirements` semantics. |
| 39 | + |
| 40 | +## Acceptance Criteria |
| 41 | +- When a PR is `mergeable_state=blocked` and the only non‑success statuses are `digger/apply` and `*/apply`, `digger apply` proceeds (no "not currently mergeable" error). |
| 42 | +- If any other non‑success status exists (e.g., tests, lint, `digger/plan`, etc.), `digger apply` remains blocked. |
| 43 | +- Auto-merge flow is unchanged and still requires combined status `success`. |
| 44 | +- Logs include helpful diagnostics showing the evaluated mergeable state and non‑success contexts. |
| 45 | +- Documentation reflects the new behavior and updates the known-issues note. |
| 46 | + |
| 47 | +## Implementation Plan |
| 48 | + |
| 49 | +1) Add pure evaluator (unit-testable) |
| 50 | +- File: `libs/ci/github/github.go` |
| 51 | +- Function: `isMergeableForApply(mergeable bool, mergeableState string, nonSuccessContexts []string) bool` |
| 52 | + - Return true if `(mergeable && isMergeableState(mergeableState))` OR `(mergeableState == "blocked" && all nonSuccessContexts ∈ {"digger/apply" ∪ contexts with suffix "/apply"})`. |
| 53 | + - Treat statuses with state in {`pending`, `failure`, `error`} as non‑success. |
| 54 | + |
| 55 | +2) Gather non‑success contexts from GitHub |
| 56 | +- In `GithubService.IsMergeable(prNumber)`: |
| 57 | + - Fetch PR via REST (already done). |
| 58 | + - Fetch combined statuses for the PR head SHA (we already have `GetCombinedPullRequestStatus`; add a sibling helper to return raw contexts and their states). |
| 59 | + - Build `nonSuccessContexts` from the combined statuses where state != `success`. |
| 60 | + - Return `isMergeableForApply(pr.GetMergeable(), pr.GetMergeableState(), nonSuccessContexts)`. |
| 61 | + - Add `slog.Debug` with fields: `mergeable`, `mergeableState`, and `nonSuccessContexts`. |
| 62 | + |
| 63 | +3) Wire through existing gates (no interface changes) |
| 64 | +- `cli/pkg/digger/digger.go`: keep using `prService.IsMergeable()` (behavior now includes the new allowance under GitHub). |
| 65 | +- `libs/apply_requirements/apply_requirements.go`: no changes, it will inherit new behavior. |
| 66 | + |
| 67 | +4) Tests |
| 68 | +- Add unit tests for `isMergeableForApply` (table‑driven) covering: |
| 69 | + - mergeable true + clean/unstable |
| 70 | + - blocked + only `digger/apply` |
| 71 | + - blocked + `projectA/apply` and `projectB/apply` |
| 72 | + - blocked + additional failing context (e.g., `ci/test`) → false |
| 73 | + - non‑blocked + mergeable=false → false |
| 74 | +- Optional: add an integration-ish test for `GithubService.IsMergeable` using a small seam to inject combined status data (or validate via a thin wrapper if direct client mocking is hard). |
| 75 | + |
| 76 | +5) Docs |
| 77 | +- Update `docs/ce/howto/apply-requirements.mdx`: |
| 78 | + - Adjust the note on mergeability to state that Digger now proceeds with apply if the only blocker is Digger’s own apply status. |
| 79 | + - Keep guidance about avoiding circular required checks for other providers or check-run based integrations. |
| 80 | + |
| 81 | +## Code Pointers |
| 82 | +- GitHub mergeability and statuses: |
| 83 | + - `libs/ci/github/github.go`: `IsMergeable`, `GetCombinedPullRequestStatus`, `isMergeableState` |
| 84 | +- Apply gates: |
| 85 | + - `cli/pkg/digger/digger.go`: mergeability check before apply |
| 86 | + - `libs/apply_requirements/apply_requirements.go`: mergeable requirement |
| 87 | +- Status producers: |
| 88 | + - `backend/utils/github.go`: sets aggregate `digger/plan` and `digger/apply` |
| 89 | + - per‑project statuses: same file via `job.GetProjectAlias()+"/apply"` |
| 90 | + |
| 91 | +## Risks & Mitigations |
| 92 | +- Missed non‑success contexts (Checks API): We only inspect commit statuses; if users require check runs (not statuses), behavior remains unchanged. Document this and consider future enhancement for Checks API. |
| 93 | +- Over‑permissive in rare cases: If other blockers do not surface as statuses (e.g., code owners, required reviews) and users don’t add `approved` requirement, apply might run despite reviews missing. This aligns with current semantics and can be controlled via `apply_requirements`. |
| 94 | + |
| 95 | +## Rollout |
| 96 | +- Feature is effectively on by default via updated `IsMergeable` logic for GitHub only. |
| 97 | +- Observe logs for `nonSuccessContexts` on real repos; rollback by reverting the helper usage if needed. |
| 98 | + |
| 99 | +## Verification Steps |
| 100 | +- Scenario A: Only `digger/apply` pending |
| 101 | + - Observe `mergeable_state=blocked`; statuses show only `digger/apply` (and/or `*/apply`) pending → `digger apply` runs. |
| 102 | +- Scenario B: Additional pending status (e.g., tests) |
| 103 | + - With `ci/test` pending → `digger apply` remains blocked. |
| 104 | +- Scenario C: Clean/unstable |
| 105 | + - `mergeable=true`, `state=clean|unstable` → unchanged, `digger apply` runs. |
| 106 | + |
0 commit comments