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
1 change: 1 addition & 0 deletions changelog.d/changed-acmm-merge-queue-capability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The ACMM `acmm:merge-queue` check now tests the capability on the default branch instead of a file name ([#10891](https://github.com/hivecommons/hive/issues/10891), part of [#10884](https://github.com/hivecommons/hive/issues/10884)). It passes, and counts toward Level 6, when GitHub's merge queue is on ("satisfied by GitHub merge queue") or when the repo's `merge_strategy` is `hive-serialized` and the branch has a known, non-empty set of required checks ("satisfied by Hive serialized merge lane"). A pass that rests only on a marker file (`.github/workflows/merge-queue.yml`, `.github/merge-queue.yml`, `.prow.yaml`, `tide.yaml`) keeps passing, labelled "file only, not verified", so no repo loses readiness. A failing row offers an owner the action to use the serialized merge lane. On personal-account repositories, where GitHub's merge queue is unavailable, the gap ticket (single or "Open Issues for L6") explains the serialized merge lane and required checks instead of asking for a file and no longer carries `ai-fix-requested`; organization repositories get the same ticket as before. The tooltip and the Level 6 description now read "a merge queue or equivalent serialized merging".
25 changes: 25 additions & 0 deletions src/docs/acmm-waivers.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,31 @@ In the multi-repo aggregate a criterion is marked waived only when *no* repo
detected the real thing — one repo holding the file makes the fleet-wide claim
true on its own.

## Hive-verified credit is not a waiver

Some criteria Hive can verify itself, from evidence a repository cannot
author. That credit lands in the result's `satisfied_by` field (not
`waiver_satisfied_by`), the row reads "satisfied by …", and unlike a waiver it
**does** count toward the level:

- `acmm:github-actions-ai` — a managed repo with Hive agent activity in the
last 30 days ("satisfied by Hive agent loop").
- `acmm:merge-queue` — tested as a capability on the default branch, not a
file name: GitHub's merge queue is on ("satisfied by GitHub merge queue"),
or the repo's `merge_strategy` is `hive-serialized` and the branch has a
known, non-empty set of required checks ("satisfied by Hive serialized
merge lane"). A repo that passes only because one of the marker files
exists keeps passing and keeps scoring, but the row is labelled
"file only, not verified". On a failing row the dashboard offers an owner
the action to use the serialized merge lane (it sets the repo's
`merge_strategy` to `hive-serialized`). For a repository owned by a personal account,
where GitHub's merge queue is unavailable, the gap ticket explains the
serialized merge lane and required checks instead of asking for a file, and
carries only the `acmm` label (no `ai-fix-requested`: the fix is an owner
setting). Organization repositories get the same ticket as before. The
check costs three read-only calls per repo per evaluation (the repository,
classic branch protection and the branch's rules).

## Cost

Effectively zero for repos that do not use it. `prefetchDirectories` already
Expand Down
204 changes: 102 additions & 102 deletions src/docs/api-reference.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/pkg/config/repo_merge_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func TestInvalidMergeStrategyFailsValidationAndStopsAutoMerge(t *testing.T) {
cfg.Project.Org = "acme"
cfg.GitHub.Token = "x"
cfg.ACMMLevel = &level
cfg.Agents = map[string]AgentConfig{"scanner": {Backend: "claude"}}
cfg.Project.RepoPolicies = []RepoPolicy{{Repo: "console", MergeStrategy: "hive-serialised"}}

err := cfg.Validate()
Expand Down
146 changes: 146 additions & 0 deletions src/pkg/dashboard/acmm_merge_queue.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package dashboard

import (
"context"
"fmt"
"strings"

gh "github.com/google/go-github/v72/github"
"github.com/hivecommons/hive/pkg/config"
"github.com/hivecommons/hive/pkg/github"
)

// acmm:merge-queue is credited from the capability, not a file name (#10891).
// The criterion ID is shared with the upstream ACMM scanner and never changes.
const (
acmmMergeQueueID = "acmm:merge-queue"
// SatisfiedBy values; the dashboard renders them as "satisfied by <value>".
acmmMergeQueueProviderNative = "GitHub merge queue"
acmmMergeQueueProviderLane = "Hive serialized merge lane"
// acmmOwnerTypeUser is GitHub's owner.type for a personal account.
acmmOwnerTypeUser = "User"
)

// acmmMergeQueueCapability decides capability credit for acmm:merge-queue on
// the default branch: GitHub's native merge queue, or the hive-serialized
// strategy together with a known, non-empty required-check set.
func acmmMergeQueueCapability(strategy, branch string, rules github.BranchRulesResult) (provider, reason string, ok bool) {
if rules.MergeQueueKnown && rules.MergeQueue {
return acmmMergeQueueProviderNative, fmt.Sprintf("GitHub merge queue is on for %s", branch), true
}
if strategy == config.MergeStrategyHiveSerialized && rules.Known && len(rules.Required) > 0 {
return acmmMergeQueueProviderLane, fmt.Sprintf("merge_strategy %s with required checks on %s: %s",
config.MergeStrategyHiveSerialized, branch, strings.Join(rules.SortedRequired(), ", ")), true
}
return "", "", false
}

// acmmMergeQueueCredit reads the repo's default branch and its rules and
// returns capability credit for acmm:merge-queue. Any read failure means no
// credit; the file check (and waivers) still apply as before.
func (s *Server) acmmMergeQueueCredit(ctx context.Context, client *gh.Client, owner, repo string) (provider, reason string, ok bool) {
if client == nil || s.deps == nil || s.deps.Config == nil {
return "", "", false
}
info, _, err := client.Repositories.Get(github.WithRESTCaller(ctx, "hive:acmm_merge_queue"), owner, repo)
if err != nil || info == nil {
return "", "", false
}
branch := info.GetDefaultBranch()
if branch == "" {
return "", "", false
}
set, known := s.deps.Config.AutoMerge.RequiredCheckSet()
rules := github.ReadBranchRules(ctx, client, owner, repo, branch, set, known)
return acmmMergeQueueCapability(s.deps.Config.RepoMergeStrategy(repo), branch, rules)
}

// applyMergeQueueCredit sets capability credit on res, or marks a pass that
// rests only on a marker file. A file-only pass keeps passing (no readiness
// regression); it is only labelled.
func (s *Server) applyMergeQueueCredit(ctx context.Context, client *gh.Client, owner, repo string, res *CriterionResult) {
if provider, reason, ok := s.acmmMergeQueueCredit(ctx, client, owner, repo); ok {
res.Passed = true
res.SatisfiedBy = provider
res.SatisfiedReason = reason
return
}
if res.Passed {
res.FileOnly = true
}
}

// acmmRepoOwnedByUser reports whether owner/repo belongs to a personal
// account. Any failure to tell answers false, so the ticket stays the one
// organization repositories get today.
func (s *Server) acmmRepoOwnedByUser(ctx context.Context, owner, repo string) bool {
if s.deps == nil || s.deps.GHClient == nil {
return false
}
client := s.deps.GHClient.GoGitHub()
if client == nil {
return false
}
info, _, err := client.Repositories.Get(github.WithRESTCaller(ctx, "hive:acmm_owner_type"), owner, repo)
if err != nil || info == nil {
return false
}
return info.GetOwner().GetType() == acmmOwnerTypeUser
}

// acmmIssueLabels returns the labels a GitHub gap issue carries. The
// personal-account merge-queue ticket drops ai-fix-requested: its fix is an
// owner setting, not code an agent can write.
func acmmIssueLabels(personalMergeQueue bool) []string {
if personalMergeQueue {
return []string{acmmIssueLabelName}
}
return []string{acmmIssueLabelName, "ai-fix-requested"}
}

// acmmMergeQueuePersonalIssueContent is the gap ticket for acmm:merge-queue on
// a personal-account repository. GitHub's merge queue is not available there,
// so the ticket points at the serialized merge lane and required checks
// instead of asking for a file.
func acmmMergeQueuePersonalIssueContent(criterion *ACMMCriterion, repo string) (title, body string) {
levelName := acmmLevelNames[criterion.Level]
if levelName == "" {
levelName = "Unknown"
}
title = fmt.Sprintf("[ACMM L%d] Turn on serialized merging (%s)", criterion.Level, criterion.Name)
body = fmt.Sprintf("## ACMM Gap: %s\n\n"+
"**Level:** L%d %s\n"+
"**Category:** %s\n"+
"**Criterion ID:** `%s`\n\n"+
"### What's needed\n\n"+
"This repository is owned by a personal account. GitHub's merge queue is only "+
"available to repositories owned by an organization, so it cannot be turned on here.\n\n"+
"Hive's serialized merge lane, together with required checks, satisfies this criterion "+
"instead: when this repository's merge strategy is `%s` and its default branch has at "+
"least one required status check, the ACMM evaluation credits the criterion as "+
"\"satisfied by %s\" and it counts toward ACMM Level %d (%s).\n\n"+
"### Why it matters\n\n"+
"Serialized merging lands one pull request at a time and validates it against the "+
"current base first, so two agent pull requests that pass on their own cannot break "+
"the default branch together.\n\n"+
"### How to fix\n\n"+
"This is a setting a repository owner changes, not code:\n\n"+
"1. In the Hive dashboard's ACMM evaluation, open the %s row for `%s` and choose "+
"**Use serialized merge lane** (or `POST /api/repos/merge-strategy` with "+
"`{\"repo\": \"%s\", \"merge_strategy\": \"%s\"}`).\n"+
"2. Make sure the default branch requires at least one status check, in a branch "+
"ruleset or in branch protection.\n\n"+
"---\n"+
"*Opened by Hive ACMM Evaluation*",
criterion.Name,
criterion.Level, levelName,
criterion.Category,
criterion.ID,
config.MergeStrategyHiveSerialized,
acmmMergeQueueProviderLane,
criterion.Level, levelName,
criterion.Name, repo,
repo, config.MergeStrategyHiveSerialized,
)
return title, body
}
Loading
Loading