mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-26 18:24:54 +09:00
c3d771e23e
Keeps the interlocked pins current and reports what it could not move. Areas apply, gate and commit one at a time, so a dependency that breaks the build costs one commit rather than the whole pull request.
195 lines
5.4 KiB
Go
195 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// skipLabel lets a maintainer park the bot on a pull request without closing it.
|
|
const skipLabel = "no-autoupdate"
|
|
|
|
// closedCooldown is how long a rejected change is left alone. Reopening the
|
|
// same diff the next morning is how an automation earns itself an ignore rule.
|
|
const closedCooldown = 7 * 24 * time.Hour
|
|
|
|
var errHumanCommits = errors.New("branch has commits the bot did not push")
|
|
|
|
type prState struct {
|
|
Number int `json:"number"`
|
|
State string `json:"state"`
|
|
Body string `json:"body"`
|
|
HeadRefOid string `json:"headRefOid"`
|
|
Labels []struct {
|
|
Name string `json:"name"`
|
|
} `json:"labels"`
|
|
ClosedAt time.Time `json:"closedAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func (p *prState) hasLabel(name string) bool {
|
|
for _, l := range p.Labels {
|
|
if l.Name == name {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// lookupPR finds the most recent pull request for branch, in any state.
|
|
func lookupPR(ctx context.Context, r *repo, slug, branch string) (*prState, error) {
|
|
out, err := r.run(ctx, "gh", "pr", "list",
|
|
"--repo", slug,
|
|
"--head", branch,
|
|
"--state", "all",
|
|
"--limit", "5",
|
|
"--json", "number,state,body,headRefOid,labels,closedAt,updatedAt")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var prs []prState
|
|
if err := json.Unmarshal([]byte(out), &prs); err != nil { //nolint:noinlineerr
|
|
return nil, fmt.Errorf("decoding gh pr list: %w", err)
|
|
}
|
|
|
|
if len(prs) == 0 {
|
|
return nil, nil //nolint:nilnil // absence is the normal first-run case
|
|
}
|
|
|
|
newest := &prs[0]
|
|
for i := range prs {
|
|
if prs[i].UpdatedAt.After(newest.UpdatedAt) {
|
|
newest = &prs[i]
|
|
}
|
|
}
|
|
|
|
return newest, nil
|
|
}
|
|
|
|
// publishOptions carries everything the pull request lifecycle needs that is
|
|
// not derived from the working tree.
|
|
type publishOptions struct {
|
|
Slug string
|
|
Remote string
|
|
Branch string
|
|
Base string
|
|
Title string
|
|
Force bool
|
|
}
|
|
|
|
// publish force-pushes the rebuilt branch and puts it in front of a human:
|
|
// reusing the open pull request if there is one, opening a new one once the
|
|
// last was merged, and staying out of the way when the last was rejected.
|
|
func publish(ctx context.Context, r *repo, opt publishOptions, results []result) error {
|
|
tree, err := treeSHA(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
head, err := headSHA(ctx, r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
now := markerOf(results, tree, head)
|
|
body := renderBody(results, now)
|
|
|
|
pr, err := lookupPR(ctx, r, opt.Slug, opt.Branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if pr != nil && pr.hasLabel(skipLabel) {
|
|
log.Printf("pull request #%d is labelled %s; leaving it alone", pr.Number, skipLabel)
|
|
|
|
return nil
|
|
}
|
|
|
|
if pr != nil && pr.State == "OPEN" {
|
|
return updateOpen(ctx, r, opt, pr, body, now)
|
|
}
|
|
|
|
if pr != nil && pr.State == "CLOSED" {
|
|
prev, ok := parseMarker(pr.Body)
|
|
if ok && prev.Tree == tree && !opt.Force && time.Since(pr.ClosedAt) < closedCooldown {
|
|
log.Printf("identical tree was closed unmerged in #%d on %s; skipping",
|
|
pr.Number, pr.ClosedAt.Format(time.DateOnly))
|
|
|
|
return nil
|
|
}
|
|
|
|
body = fmt.Sprintf("Previously closed unmerged: #%d\n\n%s", pr.Number, body)
|
|
}
|
|
|
|
if err := forcePush(ctx, r, opt.Remote, opt.Branch); err != nil { //nolint:noinlineerr
|
|
return err
|
|
}
|
|
|
|
return createPR(ctx, r, opt, body)
|
|
}
|
|
|
|
// updateOpen refreshes an existing pull request in place, but refuses to
|
|
// force-push over work the bot did not author.
|
|
func updateOpen(ctx context.Context, r *repo, opt publishOptions, pr *prState, body string, now marker) error {
|
|
prev, hadMarker := parseMarker(pr.Body)
|
|
|
|
remoteTip := remoteBranchSHA(ctx, r, opt.Remote, opt.Branch)
|
|
if hadMarker && prev.Head != "" && remoteTip != "" && remoteTip != prev.Head {
|
|
_, _ = r.run(ctx, "gh", "pr", "comment", strconv.Itoa(pr.Number), "--repo", opt.Slug,
|
|
"--body", "This branch has commits the automation did not push, so it has paused. "+
|
|
"Merge or close this pull request to resume.")
|
|
|
|
return fmt.Errorf("%w: %s is at %s, expected %s", errHumanCommits, opt.Branch, remoteTip, prev.Head)
|
|
}
|
|
|
|
err := forcePush(ctx, r, opt.Remote, opt.Branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := r.run(ctx, "gh", "pr", "edit", strconv.Itoa(pr.Number), "--repo", opt.Slug, "--body", body); err != nil { //nolint:noinlineerr
|
|
return err
|
|
}
|
|
|
|
// Only speak up when the outcome actually changed. A daily "still the
|
|
// same" comment trains the reader to ignore the thread.
|
|
if hadMarker && changedAreas(prev, now) {
|
|
if _, err := r.run(ctx, "gh", "pr", "comment", strconv.Itoa(pr.Number), "--repo", opt.Slug, //nolint:noinlineerr
|
|
"--body", "The set of applied areas changed since the last run; see the updated description."); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
log.Printf("updated pull request #%d", pr.Number)
|
|
|
|
return nil
|
|
}
|
|
|
|
func createPR(ctx context.Context, r *repo, opt publishOptions, body string) error {
|
|
out, err := r.run(ctx, "gh", "pr", "create",
|
|
"--repo", opt.Slug,
|
|
"--base", opt.Base,
|
|
"--head", opt.Branch,
|
|
"--title", opt.Title,
|
|
"--body", body)
|
|
if err != nil {
|
|
// The branch is already pushed at this point, so a failure here costs
|
|
// the pull request, not the work. Say so: the usual cause is a token
|
|
// without pull request write, and the fix is a token change plus a
|
|
// re-run, not redoing the bump by hand.
|
|
return fmt.Errorf("%w (branch %s is pushed; open the pull request by hand or fix the token and re-run)",
|
|
err, opt.Branch)
|
|
}
|
|
|
|
log.Printf("opened pull request %s", strings.TrimSpace(out))
|
|
|
|
return nil
|
|
}
|