saboteur

package module
v0.0.0-...-ed22eea Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 15, 2024 License: MIT Imports: 15 Imported by: 0

README

Saboteur 💣

GitHub merge bot that merges branch fast-forward, using Git.

Why?

There is a lot of merge bots for GitHub, but all the ones I checked merge pull requests using the GitHub API. This leaves you with two options:

  1. Let the GitHub API merge your PRs using merge commits, not ideal if you prefer linear histories

  2. Let the GitHub API merge your PRs using the "squash" or "rebase" methods, which both modify your commits, not ideal if you like your GPG signatures to be preserved

Saboteur will only merge PRs that are fast forward (rebased on top of) their base branch, and will do so using a regular git push, that preserves commit SHAs.

Merge rules

Saboteur can select the pull requests to merge based on the following criteria:

  • Target branch
  • Successful checks (eg. GitHub Actions results)
  • Labels

Setup

Saboteur can authenticate itself using either personal access tokens or as a GitHub app.

Personal access tokens

Create a classic Personal Access Token with the repo scope.

Note: fine grained access tokens are currently unsupported, since they don't work well with the GraphQL API used by Saboteur.

Check the examples in saboteur.yml to see how to set the token in the config file, either directly or via an environment variable.

GitHub apps

Register a new GitHub app, setting neither a callback URL nor a webhook URL. Set the repository permissions as follows:

  • Actions: read only
  • Checks: read only
  • Commit statuses: read only
  • Content: read and write
  • Pull requests: read only

Leave organization permissions and account permissions empty.

Once the app is created, write down the application ID on top of the page. Go to the bottom of the page and generate a new private key for the app. Go to the "Install App" section, and install the app into your account. Once the app is installed, retrieve the installation ID from the URL: if the URL looks like https://github.com/organizations/my-org/settings/installations/12345678, the installation ID is 12345678.

Check the example in saboteur.yml to see where to input those values.

Is this project ready/stable?

This project is at the MVP stage: you can call it from a cron job, and it'll merge PRs as instructed. The damages it can cause should be quite limited, since it only attempts fast forward pushes.

Potential future improvements include:

  • listening to webhook events for faster reactions
  • figure out if cloning is really necessary?
  • sandboxing the git operations better, whether it is using libgit2's SQLite ODB backend, a WASM build of libgit2…

Isn't cloning a repo for each merge expensive?

According to my understanding of git, one should be able to do a git push without cloning at all (we are just asking Git to update a remote ref in a fast forward way). This doesn't seem to work in practice for reasons I don't fully understand, so Saboteur will instead do a shallow clone of your repo, using an object filter to filter out blobs (file contents). What this means is that cloning is pretty fast, eg. cloning the latest master of torvalds/linux with this method downloads less than 5MB. When GitHub implements support for additional object filters, we can make this process even more efficient (we currently clone tree objects, which we don't need)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Merge

func Merge(ctx context.Context, authHeaderFunc func(ctx context.Context) (string, error), owner, repo string, mr MergeableMR) (err error)

Types

type Auth

type Auth interface{ IsAuth() }

type AuthHeaderSource

type AuthHeaderSource func(ctx context.Context) (string, error)

AuthHeaderSource returns the value of the Authorization header for an HTTP request.

func SetupAuth

func SetupAuth(auth Auth) (apiTransport http.RoundTripper, gitAuthHeaderSource AuthHeaderSource, err error)

type AuthInstallation

type AuthInstallation struct {
	AppID          int64
	InstallationID int64
	KeyFile        string
}

func (AuthInstallation) IsAuth

func (AuthInstallation) IsAuth()

type AuthPAT

type AuthPAT struct {
	Username     string
	Token        string
	TokenFromEnv string
}

func (AuthPAT) IsAuth

func (AuthPAT) IsAuth()

type Check

type Check struct {
	WorkflowName string
	Name         string
}

func (Check) String

func (c Check) String() string

type Config

type Config struct {
	Auth         Auth `json:"-"`
	Repositories map[string]struct {
		Rules []Rule
	}
}

func LoadConfig

func LoadConfig(r io.Reader) (Config, error)

func LoadConfigFromFile

func LoadConfigFromFile(filename string) (Config, error)

func (*Config) UnmarshalJSON

func (c *Config) UnmarshalJSON(d []byte) error

type MatchResult

type MatchResult struct {
	Result bool
	Reason string
}

type MergeableMR

type MergeableMR struct {
	// Number and title just for display/debug purposes
	Number int
	Title  string

	BaseRef  string // eg. refs/heads/master
	BaseHead string // SHA of BaseRef
	Head     string // Git commit SHA
}

func ListMergeableMRs

func ListMergeableMRs(ctx context.Context, client *githubv4.Client, owner, repo string, rules []Rule) ([]MergeableMR, error)

type PR

type PR struct {
	Number  int
	Title   string
	BaseRef struct {
		Prefix string
		Name   string
		Target struct{ Oid string }
	}
	Labels  struct{ Nodes []struct{ Name string } } `graphql:"labels(first: 100)"`
	HeadRef struct {
		Target struct {
			Oid    string
			Commit struct {
				CheckSuites struct {
					Nodes []struct {
						Status      githubv4.CheckStatusState
						Conclusion  githubv4.CheckConclusionState
						WorkflowRun struct {
							Workflow struct{ Name string }
						}
						CheckRuns struct {
							Nodes []struct {
								Name string
							}
						} `graphql:"checkRuns(first: 10)"`
					}
				} `graphql:"checkSuites(first: 10)"`
			} `graphql:"... on Commit"`
		}
	}
	Commits struct {
		Nodes []struct {
			Commit struct {
				Parents struct {
					Nodes []struct {
						Oid string
					}
				} `graphql:"parents(first:100)"`
			}
		}
	} `graphql:"commits(first: 1)"`
}

type Rule

type Rule struct {
	TargetBranch     string
	SuccessfulChecks []Check
	Labels           []string
}

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL