func Ref(ref string, headFunc func() (string, error)) string {
	if ref != "" {
		return ref
	}
	ref, err := headFunc()
	if err != nil {
		return DefaultRef
	}
	return refRegex.ReplaceAllString(ref, "$1")
}
Ref attempts to return the proper git ref to deploy. If a ref is provided,
that will be returned. If not, it will fallback to calling headFunc. If an
error is returned (not in a git repo), then it will fallback to DefaultRef.
package deploy
import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"regexp"
	"strings"
	"time"
	"github.com/github/hub/git"
	hub "github.com/github/hub/github"
	"github.com/google/go-github/v35/github"
	"github.com/urfave/cli"
)
const (
	Name  = "deploy"
	Usage = "A command for creating GitHub deployments"
)
const (
	DefaultRef     = "master"
	DefaultTimeout = 20 * time.Second
)
var errTimeout = errors.New("Timed out waiting for build to start. Did you add a webhook to handle deployment events?")
func init() {
	cli.AppHelpTemplate = `USAGE:
   # Deploy the master branch of remind101/acme-inc to staging
   {{.Name}} --env=staging --ref=master remind101/acme-inc
   # Deploy HEAD of the current branch to staging
   {{.Name}} --env=staging remind101/acme-inc
   # Deploy the current GitHub repo to staging
   {{.Name}} --env=staging
{{if .Flags}}
OPTIONS:
   {{range .Flags}}{{.}}
   {{end}}{{end}}
`
}
var ProtectedEnvironments = map[string]bool{
	"production": true,
}
var flags = []cli.Flag{
	cli.StringFlag{
		Name:  "ref, branch, commit, tag",
		Value: "",
		Usage: "The git ref to deploy. Can be a git commit, branch or tag.",
	},
	cli.StringFlag{
		Name:  "env, e",
		Value: "",
		Usage: "The environment to deploy to.",
	},
	cli.BoolFlag{
		Name:  "force, f",
		Usage: "Ignore commit status checks.",
	},
	cli.BoolFlag{
		Name:  "detached, d",
		Usage: "Don't wait for the deployment to complete.",
	},
	cli.BoolFlag{
		Name:  "quiet, q",
		Usage: "Silence any output to STDOUT.",
	},
	cli.BoolFlag{
		Name:  "update, u",
		Usage: "Update the binary",
	},
}
func NewApp() *cli.App {
	app := cli.NewApp()
	app.Version = Version
	app.Name = Name
	app.Usage = Usage
	app.Flags = flags
	app.Action = func(c *cli.Context) {
		if c.Bool("update") {
			updater := NewUpdater()
			if err := updater.Update(); err != nil {
				fmt.Printf("Error Updating deploy command: %s\n", err)
				os.Exit(-1)
			} else {
				os.Exit(0)
			}
		}
		if err := RunDeploy(c); err != nil {
			msg := err.Error()
			if err, ok := err.(*github.ErrorResponse); ok {
				if strings.HasPrefix(err.Message, "Conflict: Commit status checks failed for") {
					msg = "Commit status checks failed. You can bypass commit status checks with the --force flag."
				} else if strings.HasPrefix(err.Message, "No ref found for") {
					msg = fmt.Sprintf("%s. Did you push it to GitHub?", err.Message)
				} else {
					msg = err.Message
				}
			}
			fmt.Printf("Error from github deployments: %s\n", msg)
			os.Exit(-1)
		}
	}
	return app
}
func RunDeploy(c *cli.Context) error {
	var w io.Writer
	if c.Bool("quiet") {
		w = ioutil.Discard
	} else {
		w = c.App.Writer
	}
	h, err := hub.CurrentConfig().PromptForHost("github.com")
	if err != nil {
		return err
	}
	client, err := newGitHubClient(h)
	if err != nil {
		return err
	}
	nwo, err := Repo(c.Args())
	if err != nil {
		return err
	}
	owner, repo, err := SplitRepo(nwo, os.Getenv("GITHUB_ORGANIZATION"))
	if err != nil {
		return fmt.Errorf("Invalid GitHub repo: %s", nwo)
	}
	if c.String("env") == "" {
		return fmt.Errorf("--env flag is required")
	}
	env := AliasEnvironment(c.String("env"))
	ref := Ref(c.String("ref"), git.Head)
	err = displayNewCommits(owner, repo, ref, env, client)
	if err != nil {
		return err
	}
	r, err := newDeploymentRequest(c, ref, env)
	if err != nil {
		return err
	}
	fmt.Fprintf(w, "Deploying %s/%s@%s to %s...\n", owner, repo, *r.Ref, *r.Environment)
	ctx := context.TODO()
	d, _, err := client.Repositories.CreateDeployment(ctx, owner, repo, r)
	if err != nil {
		return err
	}
	if c.Bool("detached") {
		return nil
	}
	started := make(chan *github.DeploymentStatus)
	completed := make(chan *github.DeploymentStatus)
	go func() {
		started <- waitState(pendingStates, owner, repo, *d.ID, client)
	}()
	go func() {
		completed <- waitState(completedStates, owner, repo, *d.ID, client)
	}()
	select {
	case <-time.After(DefaultTimeout):
		return errTimeout
	case status := <-started:
		var url string
		if status.TargetURL != nil {
			url = *status.TargetURL
		}
		fmt.Fprintf(w, "%s\n", url)
	}
	status := <-completed
	if isFailed(*status.State) {
		return errors.New("Failed to deploy")
	}
	return nil
}
func displayNewCommits(owner string, repo string, ref string, env string, client *github.Client) error {
	opt := &github.DeploymentsListOptions{
		Environment: env,
	}
	deployments, _, err := client.Repositories.ListDeployments(context.TODO(), owner, repo, opt)
	if err != nil {
		return err
	}
	if len(deployments) == 0 {
		return nil
	}
	sha := *deployments[0].SHA
	compare, _, err := client.Repositories.CompareCommits(context.TODO(), owner, repo, sha, ref)
	if err != nil {
		return err
	}
	if len(compare.Commits) == 0 {
		return nil
	}
	fmt.Printf("%s\n\n", "Deploying the following commits:")
	for _, commit := range compare.Commits {
		message := *commit.Commit.Message
		fmt.Printf("%-20s\t%s\n", *commit.Commit.Author.Name, strings.Split(message, "\n")[0])
	}
	fmt.Printf("\nSee entire diff here: https://github.com/%s/%s/compare/%s...%s\n\n", owner, repo, sha, ref)
	return nil
}
var EnvironmentAliases = map[string]string{
	"prod":  "production",
	"stage": "staging",
}
func AliasEnvironment(env string) string {
	if a, ok := EnvironmentAliases[env]; ok {
		return a
	}
	return env
}
func newDeploymentRequest(c *cli.Context, ref string, env string) (*github.DeploymentRequest, error) {
	if ProtectedEnvironments[env] {
		yes := askYN(fmt.Sprintf("Are you sure you want to deploy %s to %s?", ref, env))
		if !yes {
			return nil, fmt.Errorf("Deployment aborted.")
		}
	}
	var contexts *[]string
	if c.Bool("force") {
		s := []string{}
		contexts = &s
	}
	return &github.DeploymentRequest{
		Ref:              github.String(ref),
		Task:             github.String("deploy"),
		AutoMerge:        github.Bool(false),
		Environment:      github.String(env),
		RequiredContexts: contexts,
		Payload: map[string]interface{}{
			"force": c.Bool("force"),
		},
		Description: github.String("remind101/deploy CLI-initiated deploy"),
	}, nil
}
var (
	pendingStates   = []string{"pending"}
	completedStates = []string{"success", "error", "failure"}
)
func isFailed(state string) bool {
	return state == "error" || state == "failure"
}
func waitState(states []string, owner, repo string, deploymentID int64, c *github.Client) *github.DeploymentStatus {
	for {
		<-time.After(1 * time.Second)
		statuses, _, err := c.Repositories.ListDeploymentStatuses(context.TODO(), owner, repo, deploymentID, nil)
		if err != nil {
			continue
		}
		status := firstStatus(states, statuses)
		if status != nil {
			return status
		}
	}
}
func firstStatus(states []string, statuses []*github.DeploymentStatus) *github.DeploymentStatus {
	for _, ds := range statuses {
		for _, s := range states {
			if ds.State != nil && *ds.State == s {
				return ds
			}
		}
	}
	return nil
}
var refRegex = regexp.MustCompile(`^refs/heads/(.*)$`)