├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── README.md ├── api.go ├── bot.go ├── bot_test.go ├── cli └── cli.go ├── command.go ├── config.go ├── examplebot ├── extension.go └── main.go ├── go.mod ├── go.sum ├── hybrid.go ├── kbchat.go ├── keybot ├── README.md ├── keybase.keybot.plist ├── keybot.go ├── keybot.sh ├── main.go ├── main_test.go └── winbot.go ├── launchd ├── command.go ├── example │ └── schedule-command.plist ├── plist.go └── plist_test.go ├── scripts ├── check_status_and_pull.sh ├── dumplog.sh ├── git_clean.sh ├── git_diff.sh ├── node_module_clean.sh ├── release.broken.sh ├── release.promote.sh ├── run.sh ├── run_and_send_stdout.sh ├── send.sh ├── smoketest.sh └── upgrade.sh ├── send ├── README.md └── main.go ├── slack.go ├── systemd ├── README.md ├── keybase.buildplease.service ├── keybase.buildplease.timer ├── keybase.keybot.service ├── nightly-stathat-success.sh ├── prerelease.sh ├── run_keybot.sh ├── send.sh ├── tuxbot.nightly.stathat-noop.service └── tuxbot.nightly.stathat-noop.timer └── tuxbot ├── main.go ├── main_test.go └── tuxbot.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: [1.23.x] 14 | os: [ubuntu-latest, macos-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - uses: actions/checkout@v3 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.63 25 | - run: go vet ./... 26 | - run: go test ./... 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gopath 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | gocritic: 3 | disabled-checks: 4 | - ifElseChain 5 | - elseif 6 | 7 | linters: 8 | enable: 9 | - gofmt 10 | - govet 11 | - gocritic 12 | - unconvert 13 | - revive 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/gabriel/pre-commit-golang 2 | sha: c02a81d85a5295886022b8106c367518e6c3760e 3 | hooks: 4 | - id: go-fmt 5 | - id: go-metalinter 6 | args: 7 | - --deadline=60s 8 | - --vendor 9 | - --cyclo-over=20 10 | - --dupl-threshold=100 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Slackbot 2 | 3 | [![Build Status](https://github.com/keybase/slackbot/actions/workflows/ci.yml/badge.svg)](https://github.com/keybase/slackbot/actions) 4 | [![GoDoc](https://godoc.org/github.com/keybase/slackbot?status.svg)](https://godoc.org/github.com/keybase/slackbot) 5 | 6 | ``` 7 | export SLACK_TOKEN=... 8 | go install github.com/keybase/slackbot/examplebot 9 | $GOPATH/bin/examplebot 10 | ``` 11 | 12 | Then invite the bot to a channel and then post '!examplebot help'. 13 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package slackbot 5 | 6 | import "github.com/nlopes/slack" 7 | 8 | // LoadChannelIDs loads channel ids for the Slack client 9 | func LoadChannelIDs(api slack.Client) (map[string]string, error) { 10 | channels, err := api.GetChannels(true) 11 | if err != nil { 12 | return nil, err 13 | } 14 | channelIDs := make(map[string]string) 15 | for _, c := range channels { 16 | channelIDs[c.Name] = c.ID 17 | } 18 | return channelIDs, nil 19 | } 20 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package slackbot 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "log" 10 | "os" 11 | "sort" 12 | "strings" 13 | "text/tabwriter" 14 | ) 15 | 16 | type BotCommandRunner interface { 17 | RunCommand(args []string, channel string) error 18 | } 19 | 20 | type BotBackend interface { 21 | SendMessage(text string, channel string) 22 | Listen(BotCommandRunner) 23 | } 24 | 25 | // Bot describes a generic bot 26 | type Bot struct { 27 | backend BotBackend 28 | help string 29 | name string 30 | label string 31 | config Config 32 | commands map[string]Command 33 | defaultCommand Command 34 | } 35 | 36 | func NewBot(config Config, name, label string, backend BotBackend) *Bot { 37 | return &Bot{ 38 | backend: backend, 39 | config: config, 40 | commands: make(map[string]Command), 41 | name: name, 42 | label: label, 43 | } 44 | } 45 | 46 | func (b *Bot) Name() string { 47 | return b.name 48 | } 49 | 50 | func (b *Bot) Config() Config { 51 | return b.config 52 | } 53 | 54 | func (b *Bot) AddCommand(trigger string, command Command) { 55 | b.commands[trigger] = command 56 | } 57 | 58 | func (b *Bot) triggers() []string { 59 | triggers := make([]string, 0, len(b.commands)) 60 | for trigger := range b.commands { 61 | triggers = append(triggers, trigger) 62 | } 63 | sort.Strings(triggers) 64 | return triggers 65 | } 66 | 67 | // HelpMessage is the default help message for the bot 68 | func (b *Bot) HelpMessage() string { 69 | w := new(tabwriter.Writer) 70 | buf := new(bytes.Buffer) 71 | w.Init(buf, 8, 8, 8, ' ', 0) 72 | fmt.Fprintln(w, "Command\tDescription") 73 | for _, trigger := range b.triggers() { 74 | command := b.commands[trigger] 75 | fmt.Fprintf(w, "%s\t%s\n", trigger, command.Description()) 76 | } 77 | _ = w.Flush() 78 | return BlockQuote(buf.String()) 79 | } 80 | 81 | func (b *Bot) SetHelp(help string) { 82 | b.help = help 83 | } 84 | 85 | func (b *Bot) Label() string { 86 | return b.label 87 | } 88 | 89 | func (b *Bot) SetDefault(command Command) { 90 | b.defaultCommand = command 91 | } 92 | 93 | // RunCommand runs a command 94 | func (b *Bot) RunCommand(args []string, channel string) error { 95 | if len(args) == 0 || args[0] == "help" { 96 | b.sendHelpMessage(channel) 97 | return nil 98 | } 99 | 100 | command, ok := b.commands[args[0]] 101 | if !ok { 102 | if b.defaultCommand != nil { 103 | command = b.defaultCommand 104 | } else { 105 | return fmt.Errorf("Unrecognized command: %q", args) 106 | } 107 | } 108 | 109 | if args[0] != "resume" && args[0] != "config" && b.Config().Paused() { 110 | b.backend.SendMessage("I can't do that, I'm paused.", channel) 111 | return nil 112 | } 113 | 114 | go b.run(args, command, channel) 115 | return nil 116 | } 117 | 118 | func (b *Bot) run(args []string, command Command, channel string) { 119 | out, err := command.Run(channel, args) 120 | if err != nil { 121 | log.Printf("Error %s running: %#v; %s\n", err, command, out) 122 | b.backend.SendMessage(fmt.Sprintf("Oops, there was an error in %q:\n%s", strings.Join(args, " "), 123 | BlockQuote(out)), channel) 124 | return 125 | } 126 | log.Printf("Output: %s\n", out) 127 | if command.ShowResult() { 128 | b.backend.SendMessage(out, channel) 129 | } 130 | } 131 | 132 | func (b *Bot) sendHelpMessage(channel string) { 133 | help := b.help 134 | if help == "" { 135 | help = b.HelpMessage() 136 | } 137 | b.backend.SendMessage(help, channel) 138 | } 139 | 140 | func (b *Bot) SendMessage(text string, channel string) { 141 | b.backend.SendMessage(text, channel) 142 | } 143 | 144 | func (b *Bot) Listen() { 145 | b.backend.Listen(b) 146 | } 147 | 148 | // NewTestBot returns a bot for testing 149 | func NewTestBot() (*Bot, error) { 150 | backend := &SlackBotBackend{} 151 | return NewBot(NewConfig(true, false), "testbot", "", backend), nil 152 | } 153 | 154 | // BlockQuote returns the string block-quoted 155 | func BlockQuote(s string) string { 156 | if !strings.HasSuffix(s, "\n") { 157 | s += "\n" 158 | } 159 | return "```\n" + s + "```" 160 | } 161 | 162 | // GetTokenFromEnv returns slack token from the environment 163 | func GetTokenFromEnv() string { 164 | token := os.Getenv("SLACK_TOKEN") 165 | if token == "" { 166 | log.Fatal("SLACK_TOKEN is not set") 167 | } 168 | return token 169 | } 170 | 171 | func isSpace(r rune) bool { 172 | switch r { 173 | case ' ', '\t', '\r', '\n': 174 | return true 175 | } 176 | return false 177 | } 178 | 179 | func parseInput(s string) []string { 180 | buf := "" 181 | args := []string{} 182 | var escaped, doubleQuoted, singleQuoted bool 183 | for _, r := range s { 184 | if escaped { 185 | buf += string(r) 186 | escaped = false 187 | continue 188 | } 189 | 190 | if r == '\\' { 191 | if singleQuoted { 192 | buf += string(r) 193 | } else { 194 | escaped = true 195 | } 196 | continue 197 | } 198 | 199 | if isSpace(r) { 200 | if singleQuoted || doubleQuoted { 201 | buf += string(r) 202 | } else if buf != "" { 203 | args = append(args, buf) 204 | buf = "" 205 | } 206 | continue 207 | } 208 | 209 | switch r { 210 | case '"': 211 | if !singleQuoted { 212 | doubleQuoted = !doubleQuoted 213 | continue 214 | } 215 | case '\'': 216 | if !doubleQuoted { 217 | singleQuoted = !singleQuoted 218 | continue 219 | } 220 | } 221 | 222 | buf += string(r) 223 | } 224 | if buf != "" { 225 | args = append(args, buf) 226 | } 227 | return args 228 | } 229 | -------------------------------------------------------------------------------- /bot_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package slackbot 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestHelp(t *testing.T) { 11 | bot, err := NewTestBot() 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | bot.AddCommand("date", NewExecCommand("/bin/date", nil, true, "Show the current date", &config{})) 16 | bot.AddCommand("utc", NewExecCommand("/bin/date", []string{"-u"}, true, "Show the current date (utc)", &config{})) 17 | msg := bot.HelpMessage() 18 | if msg == "" { 19 | t.Fatal("No help message") 20 | } 21 | t.Logf("Help:\n%s", msg) 22 | } 23 | 24 | func TestParseInput(t *testing.T) { 25 | args := parseInput(`!keybot dumplog "release promote"`) 26 | if args[0] != "!keybot" || args[1] != "dumplog" || args[2] != `release promote` { 27 | t.Fatal("Invalid parse") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cli/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package cli 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "log" 11 | "strings" 12 | 13 | "github.com/keybase/slackbot" 14 | 15 | "gopkg.in/alecthomas/kingpin.v2" 16 | ) 17 | 18 | // IsParseContextValid checks if the kingpin context is valid 19 | func IsParseContextValid(app *kingpin.Application, args []string) error { 20 | if pcontext, perr := app.ParseContext(args); pcontext == nil { 21 | return perr 22 | } 23 | return nil 24 | } 25 | 26 | // Parse kingpin args and return valid command, usage, and error 27 | func Parse(app *kingpin.Application, args []string, stringBuffer *bytes.Buffer) (string, string, error) { 28 | log.Printf("Parsing args: %#v", args) 29 | // Make sure context is valid otherwise showing Usage on error will fail later. 30 | // This is a workaround for a kingpin bug. 31 | if err := IsParseContextValid(app, args); err != nil { 32 | return "", "", err 33 | } 34 | 35 | cmd, err := app.Parse(args) 36 | 37 | if err != nil && stringBuffer.Len() == 0 { 38 | log.Printf("Error in parsing command: %s. got %s", args, err) 39 | _, _ = io.WriteString(stringBuffer, fmt.Sprintf("I don't know what you mean by `%s`.\nError: `%s`\nHere's my usage:\n\n", strings.Join(args, " "), err.Error())) 40 | // Print out help page if there was an error parsing command 41 | app.Usage([]string{}) 42 | } 43 | 44 | if stringBuffer.Len() > 0 { 45 | return "", slackbot.BlockQuote(stringBuffer.String()), nil 46 | } 47 | 48 | return cmd, "", err 49 | } 50 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package slackbot 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | // Command is the interface the bot uses to run things 12 | type Command interface { 13 | Run(channel string, args []string) (string, error) 14 | ShowResult() bool // Whether to output result back to channel 15 | Description() string 16 | } 17 | 18 | // execCommand is a Command that does an exec.Command(...) on the system 19 | type execCommand struct { 20 | exec string // Command to execute 21 | args []string // Args for exec.Command 22 | showResult bool 23 | description string 24 | config Config 25 | } 26 | 27 | // NewExecCommand creates an ExecCommand 28 | func NewExecCommand(exec string, args []string, showResult bool, description string, config Config) Command { 29 | return execCommand{ 30 | exec: exec, 31 | args: args, 32 | showResult: showResult, 33 | description: description, 34 | config: config, 35 | } 36 | } 37 | 38 | // Run runs the exec command 39 | func (c execCommand) Run(_ string, _ []string) (string, error) { 40 | if c.config.DryRun() { 41 | return fmt.Sprintf("I'm in dry run mode. I would have run `%s` with args: %s", c.exec, c.args), nil 42 | } 43 | 44 | out, err := exec.Command(c.exec, c.args...).CombinedOutput() 45 | outAsString := string(out) 46 | return outAsString, err 47 | } 48 | 49 | // ShowResult decides whether to show the results from the exec 50 | func (c execCommand) ShowResult() bool { 51 | return c.config.DryRun() || c.showResult 52 | } 53 | 54 | // Description describes the command 55 | func (c execCommand) Description() string { 56 | return c.description 57 | } 58 | 59 | // CommandFn is the function that is run for this command 60 | type CommandFn func(channel string, args []string) (string, error) 61 | 62 | // NewFuncCommand creates a new function command 63 | func NewFuncCommand(fn CommandFn, desc string, config Config) Command { 64 | return funcCommand{ 65 | fn: fn, 66 | desc: desc, 67 | config: config, 68 | } 69 | } 70 | 71 | type funcCommand struct { 72 | desc string 73 | fn CommandFn 74 | config Config 75 | } 76 | 77 | func (c funcCommand) Run(channel string, args []string) (string, error) { 78 | return c.fn(channel, args) 79 | } 80 | 81 | func (c funcCommand) ShowResult() bool { 82 | return true 83 | } 84 | 85 | func (c funcCommand) Description() string { 86 | return c.desc 87 | } 88 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package slackbot 5 | 6 | import ( 7 | "encoding/json" 8 | "log" 9 | "os" 10 | "os/user" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // Config is the state of the build bot 16 | type Config interface { 17 | // Paused will prevent any commands from running 18 | Paused() bool 19 | // SetPaused changes paused 20 | SetPaused(paused bool) 21 | // DryRun will print out what it plans to do without doing it 22 | DryRun() bool 23 | // SetDryRun changes dry run 24 | SetDryRun(dryRun bool) 25 | // Save persists config 26 | Save() error 27 | } 28 | 29 | type config struct { 30 | // These must be public for json serialization. 31 | DryRunField bool 32 | PausedField bool 33 | } 34 | 35 | // Paused if paused 36 | func (c config) Paused() bool { 37 | return c.PausedField 38 | } 39 | 40 | // DryRun if dry run enabled 41 | func (c config) DryRun() bool { 42 | return c.DryRunField 43 | } 44 | 45 | // SetPaused changes paused 46 | func (c *config) SetPaused(paused bool) { 47 | c.PausedField = paused 48 | } 49 | 50 | // SetDryRun changes dry run 51 | func (c *config) SetDryRun(dryRun bool) { 52 | c.DryRunField = dryRun 53 | } 54 | 55 | func getConfigPath() (string, error) { 56 | currentUser, err := user.Current() 57 | 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | return filepath.Join(currentUser.HomeDir, ".keybot"), nil 63 | } 64 | 65 | // NewConfig returns default config 66 | func NewConfig(dryRun, paused bool) Config { 67 | return &config{ 68 | DryRunField: dryRun, 69 | PausedField: paused, 70 | } 71 | } 72 | 73 | // ReadConfigOrDefault returns config stored or default 74 | func ReadConfigOrDefault() Config { 75 | cfg := readConfigOrDefault() 76 | return &cfg 77 | } 78 | 79 | func readConfigOrDefault() config { 80 | defaultConfig := config{ 81 | DryRunField: true, 82 | PausedField: false, 83 | } 84 | 85 | path, err := getConfigPath() 86 | 87 | if err != nil { 88 | return defaultConfig 89 | } 90 | 91 | fileBytes, err := os.ReadFile(path) 92 | 93 | if err != nil { 94 | return defaultConfig 95 | } 96 | 97 | var cfg config 98 | err = json.Unmarshal(fileBytes, &cfg) 99 | if err != nil { 100 | log.Printf("Couldn't read config file: %s\n", err) 101 | return defaultConfig 102 | } 103 | 104 | return cfg 105 | } 106 | 107 | func (c config) Save() error { 108 | b, err := json.Marshal(c) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | path, err := getConfigPath() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | err = os.WriteFile(path, b, 0644) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return nil 124 | } 125 | 126 | // NewShowConfigCommand returns command that shows config 127 | func NewShowConfigCommand(config Config) Command { 128 | return &showConfigCommand{config: config} 129 | } 130 | 131 | type showConfigCommand struct { 132 | config Config 133 | } 134 | 135 | func (c showConfigCommand) Run(_ string, _ []string) (string, error) { 136 | if !c.config.Paused() && !c.config.DryRun() { 137 | return "I'm running normally.", nil 138 | } 139 | lines := []string{} 140 | if c.config.Paused() { 141 | lines = append(lines, "I'm paused.") 142 | } 143 | if c.config.DryRun() { 144 | lines = append(lines, "I'm in dry run mode.") 145 | } 146 | return strings.Join(lines, " "), nil 147 | } 148 | 149 | func (c showConfigCommand) ShowResult() bool { 150 | return true 151 | } 152 | 153 | func (c showConfigCommand) Description() string { 154 | return "Shows config" 155 | } 156 | 157 | // NewToggleDryRunCommand returns toggle dry run command 158 | func NewToggleDryRunCommand(config Config) Command { 159 | return &toggleDryRunCommand{config: config} 160 | } 161 | 162 | type toggleDryRunCommand struct { 163 | config Config 164 | } 165 | 166 | func (c *toggleDryRunCommand) Run(_ string, _ []string) (string, error) { 167 | c.config.SetDryRun(!c.config.DryRun()) 168 | err := c.config.Save() 169 | if err != nil { 170 | return "", err 171 | } 172 | 173 | if c.config.DryRun() { 174 | return "We are in dry run mode.", nil 175 | } 176 | return "We are not longer in dry run mode", nil 177 | } 178 | 179 | func (c toggleDryRunCommand) ShowResult() bool { 180 | return true 181 | } 182 | 183 | func (c toggleDryRunCommand) Description() string { 184 | return "Toggles the dry run mode" 185 | } 186 | 187 | // NewPauseCommand pauses 188 | func NewPauseCommand(config Config) Command { 189 | return &pauseCommand{ 190 | config: config, 191 | pauses: true, 192 | } 193 | } 194 | 195 | // NewResumeCommand resumes 196 | func NewResumeCommand(config Config) Command { 197 | return &pauseCommand{ 198 | config: config, 199 | pauses: false, 200 | } 201 | } 202 | 203 | type pauseCommand struct { 204 | config Config 205 | pauses bool 206 | } 207 | 208 | // Run toggles the dry run state. (Itself is never run under dry run mode) 209 | func (c *pauseCommand) Run(_ string, _ []string) (string, error) { 210 | log.Printf("Setting paused: %v\n", c.pauses) 211 | c.config.SetPaused(c.pauses) 212 | err := c.config.Save() 213 | if err != nil { 214 | return "", err 215 | } 216 | 217 | if c.config.Paused() { 218 | return "I am paused.", nil 219 | } 220 | return "I have resumed.", nil 221 | } 222 | 223 | // ShowResult always shows results for toggling dry run 224 | func (c pauseCommand) ShowResult() bool { 225 | return true 226 | } 227 | 228 | // Description describes what it does 229 | func (c pauseCommand) Description() string { 230 | if c.pauses { 231 | return "Pauses the bot" 232 | } 233 | return "Resumes the bot" 234 | } 235 | -------------------------------------------------------------------------------- /examplebot/extension.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | 10 | "github.com/keybase/slackbot" 11 | "github.com/keybase/slackbot/cli" 12 | kingpin "gopkg.in/alecthomas/kingpin.v2" 13 | ) 14 | 15 | type extension struct{} 16 | 17 | func (e *extension) Run(bot *slackbot.Bot, _ string, args []string) (string, error) { 18 | app := kingpin.New("examplebot", "Kingpin extension") 19 | app.Terminate(nil) 20 | stringBuffer := new(bytes.Buffer) 21 | app.Writer(stringBuffer) 22 | 23 | testCmd := app.Command("echo", "Echo") 24 | testCmdEchoFlag := testCmd.Flag("output", "Output to echo").Required().String() 25 | 26 | cmd, usage, cmdErr := cli.Parse(app, args, stringBuffer) 27 | if usage != "" || cmdErr != nil { 28 | return usage, cmdErr 29 | } 30 | 31 | if bot.Config().DryRun() { 32 | return fmt.Sprintf("I would have run: `%#v`", cmd), nil 33 | } 34 | 35 | if cmd == testCmd.FullCommand() { 36 | return *testCmdEchoFlag, nil 37 | } 38 | return cmd, nil 39 | } 40 | 41 | func (e *extension) Help(bot *slackbot.Bot) string { 42 | out, err := e.Run(bot, "", nil) 43 | if err != nil { 44 | return fmt.Sprintf("Error getting help: %s", err) 45 | } 46 | return out 47 | } 48 | -------------------------------------------------------------------------------- /examplebot/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/keybase/slackbot" 10 | ) 11 | 12 | func main() { 13 | config := slackbot.NewConfig(false, false) 14 | backend, err := slackbot.NewSlackBotBackend(slackbot.GetTokenFromEnv()) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | bot := slackbot.NewBot(config, "examplebot", "", backend) 19 | 20 | // Command that runs and shows date 21 | bot.AddCommand("date", slackbot.NewExecCommand("/bin/date", nil, true, "Show the current date", config)) 22 | 23 | // Commands for config, pausing and doing dry runs 24 | bot.AddCommand("pause", slackbot.NewPauseCommand(config)) 25 | bot.AddCommand("resume", slackbot.NewResumeCommand(config)) 26 | bot.AddCommand("config", slackbot.NewShowConfigCommand(config)) 27 | bot.AddCommand("toggle-dryrun", slackbot.NewToggleDryRunCommand(bot.Config())) 28 | 29 | // Extension as default command with help 30 | ext := &extension{} 31 | runFn := func(channel string, args []string) (string, error) { 32 | return ext.Run(bot, channel, args) 33 | } 34 | bot.SetDefault(slackbot.NewFuncCommand(runFn, "Extension", bot.Config())) 35 | bot.SetHelp(bot.HelpMessage() + "\n\n" + ext.Help(bot)) 36 | 37 | // Connect to slack and listen 38 | bot.Listen() 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keybase/slackbot 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/keybase/go-keybase-chat-bot v0.0.0-20250106203511-859265729a56 9 | github.com/nlopes/slack v0.1.1-0.20180101221843-107290b5bbaf 10 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 11 | ) 12 | 13 | require ( 14 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 15 | github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | github.com/stretchr/testify v1.10.0 // indirect 21 | golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect 22 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 23 | gopkg.in/yaml.v2 v2.4.0 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | 27 | // keybase maintained forks 28 | replace ( 29 | bazil.org/fuse => github.com/keybase/fuse v0.0.0-20210104232444-d36009698767 30 | bitbucket.org/ww/goautoneg => github.com/adjust/goautoneg v0.0.0-20150426214442-d788f35a0315 31 | github.com/stellar/go => github.com/keybase/stellar-org v0.0.0-20191010205648-0fc3bfe3dfa7 32 | github.com/syndtr/goleveldb => github.com/keybase/goleveldb v1.0.1-0.20211106225230-2a53fac0721c 33 | gopkg.in/src-d/go-billy.v4 => github.com/keybase/go-billy v3.1.1-0.20180828145748-b5a7b7bc2074+incompatible 34 | gopkg.in/src-d/go-git.v4 => github.com/keybase/go-git v4.0.0-rc9.0.20190209005256-3a78daa8ce8e+incompatible 35 | mvdan.cc/xurls/v2 => github.com/keybase/xurls/v2 v2.0.1-0.20190725180013-1e015cacd06c 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc= 4 | github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/keybase/go-keybase-chat-bot v0.0.0-20250106203511-859265729a56 h1:w8ikAizh5hbXZxBXbees5iOxOoi7nH/qp1lJQ3pOPiY= 10 | github.com/keybase/go-keybase-chat-bot v0.0.0-20250106203511-859265729a56/go.mod h1:cmXzSxB8TNJdxMKcmywTHsbv+H3WZ/92lP9nyEbCGNQ= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 16 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 17 | github.com/nlopes/slack v0.1.1-0.20180101221843-107290b5bbaf h1:UcWHjpjwyOso8FWoVd9IqzqERjPeXvpKG9L9pzsWDnE= 18 | github.com/nlopes/slack v0.1.1-0.20180101221843-107290b5bbaf/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 23 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 24 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 25 | golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= 26 | golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 27 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 28 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 31 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 33 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 34 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | -------------------------------------------------------------------------------- /hybrid.go: -------------------------------------------------------------------------------- 1 | package slackbot 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type hybridRunner struct { 8 | runner BotCommandRunner 9 | channel string 10 | } 11 | 12 | func newHybridRunner(runner BotCommandRunner, channel string) *hybridRunner { 13 | return &hybridRunner{ 14 | runner: runner, 15 | channel: channel, 16 | } 17 | } 18 | 19 | func (r *hybridRunner) RunCommand(args []string, _ string) error { 20 | return r.runner.RunCommand(args, r.channel) 21 | 22 | } 23 | 24 | type HybridBackendMember struct { 25 | Backend BotBackend 26 | Channel string 27 | } 28 | 29 | type HybridBackend struct { 30 | backends []HybridBackendMember 31 | } 32 | 33 | func NewHybridBackend(backends ...HybridBackendMember) *HybridBackend { 34 | return &HybridBackend{ 35 | backends: backends, 36 | } 37 | } 38 | 39 | func (b *HybridBackend) SendMessage(text string, _ string) { 40 | for _, backend := range b.backends { 41 | backend.Backend.SendMessage(text, backend.Channel) 42 | } 43 | } 44 | 45 | func (b *HybridBackend) Listen(runner BotCommandRunner) { 46 | var wg sync.WaitGroup 47 | for _, backend := range b.backends { 48 | wg.Add(1) 49 | go func(b HybridBackendMember) { 50 | b.Backend.Listen(newHybridRunner(runner, b.Channel)) 51 | wg.Done() 52 | }(backend) 53 | } 54 | wg.Wait() 55 | } 56 | -------------------------------------------------------------------------------- /kbchat.go: -------------------------------------------------------------------------------- 1 | package slackbot 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/keybase/go-keybase-chat-bot/kbchat" 8 | "github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1" 9 | ) 10 | 11 | type KeybaseChatBotBackend struct { 12 | name string 13 | convID chat1.ConvIDStr 14 | kbc *kbchat.API 15 | } 16 | 17 | func NewKeybaseChatBotBackend(name string, convID string, opts kbchat.RunOptions) (BotBackend, error) { 18 | var err error 19 | bot := &KeybaseChatBotBackend{ 20 | convID: chat1.ConvIDStr(convID), 21 | name: name, 22 | } 23 | if bot.kbc, err = kbchat.Start(opts); err != nil { 24 | return nil, err 25 | } 26 | return bot, nil 27 | } 28 | 29 | func (b *KeybaseChatBotBackend) SendMessage(text string, convID string) { 30 | if chat1.ConvIDStr(convID) != b.convID { 31 | // bail out if not on configured conv ID 32 | log.Printf("SendMessage: refusing to send on non-configured convID: %s != %s\n", convID, b.convID) 33 | return 34 | } 35 | if len(text) == 0 { 36 | log.Printf("SendMessage: skipping blank message") 37 | return 38 | } 39 | log.Printf("sending message: convID: %s text: %s", convID, text) 40 | if _, err := b.kbc.SendMessageByConvID(chat1.ConvIDStr(convID), "%s", text); err != nil { 41 | log.Printf("SendMessage: failed to send: %s\n", err) 42 | } 43 | } 44 | 45 | func (b *KeybaseChatBotBackend) Listen(runner BotCommandRunner) { 46 | sub, err := b.kbc.ListenForNewTextMessages() 47 | if err != nil { 48 | panic(fmt.Sprintf("failed to set up listen: %s", err)) 49 | } 50 | commandPrefix := "!" + b.name 51 | for { 52 | msg, err := sub.Read() 53 | if err != nil { 54 | log.Printf("Listen: failed to read message: %s", err) 55 | continue 56 | } 57 | if msg.Message.Content.TypeName != "text" { 58 | continue 59 | } 60 | args := parseInput(msg.Message.Content.Text.Body) 61 | if len(args) > 0 && args[0] == commandPrefix && b.convID == msg.Message.ConvID { 62 | cmd := args[1:] 63 | if err := runner.RunCommand(cmd, string(b.convID)); err != nil { 64 | log.Printf("unable to run command: %s", err) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /keybot/README.md: -------------------------------------------------------------------------------- 1 | Some random notes: 2 | 3 | The android keys are in kbfs so make sure the keybot has keybase running (could move to encrypted git someday) 4 | The bot using launch agents, so look at the plist files in ~/Library/LaunchAgents. When builds kick off it does it through launch agents as well 5 | There are multiple go-paths that exist. The bot runs in ~/go. android builds run from ~/go-android and ios runs from ~/go-ios. The yarn rn-gobuild-* also runs in /tmp like client does 6 | The bot delegates to client's build and publish scripts under packaging so look there too 7 | -------------------------------------------------------------------------------- /keybot/keybase.keybot.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | keybase.keybot 7 | EnvironmentVariables 8 | 9 | GOPATH 10 | /Users/test/go 11 | SLACK_TOKEN 12 | 13 | SLACK_CHANNEL 14 | 15 | GITHUB_TOKEN 16 | 17 | AWS_ACCESS_KEY 18 | 19 | AWS_SECRET_KEY 20 | 21 | KEYBASE_TOKEN 22 | 23 | BOT_NAME 24 | keybot 25 | PATH 26 | /sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin 27 | 28 | ProgramArguments 29 | 30 | /bin/bash 31 | /Users/test/go/src/github.com/keybase/slackbot/keybot/keybot.sh 32 | 33 | KeepAlive 34 | 35 | RunAtLoad 36 | 37 | StandardErrorPath 38 | /Users/test/Library/Logs/keybase.keybot.log 39 | StandardOutPath 40 | /Users/test/Library/Logs/keybase.keybot.log 41 | 42 | 43 | -------------------------------------------------------------------------------- /keybot/keybot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/keybase/slackbot" 15 | "github.com/keybase/slackbot/cli" 16 | "github.com/keybase/slackbot/launchd" 17 | kingpin "gopkg.in/alecthomas/kingpin.v2" 18 | ) 19 | 20 | type keybot struct{} 21 | 22 | func (k *keybot) Run(bot *slackbot.Bot, channel string, args []string) (string, error) { 23 | app := kingpin.New("keybot", "Job command parser for keybot") 24 | app.Terminate(nil) 25 | stringBuffer := new(bytes.Buffer) 26 | app.Writer(stringBuffer) 27 | 28 | build := app.Command("build", "Build things") 29 | 30 | cancel := app.Command("cancel", "Cancel") 31 | cancelLabel := cancel.Arg("label", "Launchd job label").String() 32 | 33 | buildMobile := build.Command("mobile", "Start an iOS and Android build") 34 | buildMobileSkipCI := buildMobile.Flag("skip-ci", "Whether to skip CI").Bool() 35 | buildMobileAutomated := buildMobile.Flag("automated", "Whether this is a timed build").Bool() 36 | buildMobileCientCommit := buildMobile.Flag("client-commit", "Build a specific client commit hash").String() 37 | 38 | buildAndroid := build.Command("android", "Start an android build") 39 | buildAndroidSkipCI := buildAndroid.Flag("skip-ci", "Whether to skip CI").Bool() 40 | buildAndroidAutomated := buildAndroid.Flag("automated", "Whether this is a timed build").Bool() 41 | buildAndroidCientCommit := buildAndroid.Flag("client-commit", "Build a specific client commit hash").String() 42 | buildIOS := build.Command("ios", "Start an ios build") 43 | buildIOSClean := buildIOS.Flag("clean", "Whether to clean first").Bool() 44 | buildIOSSkipCI := buildIOS.Flag("skip-ci", "Whether to skip CI").Bool() 45 | buildIOSAutomated := buildIOS.Flag("automated", "Whether this is a timed build").Bool() 46 | buildIOSCientCommit := buildIOS.Flag("client-commit", "Build a specific client commit hash").String() 47 | 48 | buildDarwin := build.Command("darwin", "Start a darwin build") 49 | buildDarwinTest := buildDarwin.Flag("test", "Whether build is for testing").Bool() 50 | buildDarwinClientCommit := buildDarwin.Flag("client-commit", "Build a specific client commit").String() 51 | buildDarwinKbfsCommit := buildDarwin.Flag("kbfs-commit", "Build a specific kbfs commit").String() 52 | buildDarwinNoPull := buildDarwin.Flag("skip-pull", "Don't pull before building the app").Bool() 53 | buildDarwinSkipCI := buildDarwin.Flag("skip-ci", "Whether to skip CI").Bool() 54 | buildDarwinSmoke := buildDarwin.Flag("smoke", "Whether to make a pair of builds for smoketesting when on a branch").Bool() 55 | buildDarwinNoS3 := buildDarwin.Flag("skip-s3", "Don't push to S3 after building the app").Bool() 56 | buildDarwinNoNotarize := buildDarwin.Flag("skip-notarize", "Don't notarize the app").Bool() 57 | 58 | release := app.Command("release", "Release things") 59 | releasePromote := release.Command("promote", "Promote a release to public") 60 | releaseToPromotePlatform := releasePromote.Arg("platform", "Platform to promote a release for").Required().String() 61 | releaseToPromote := releasePromote.Arg("release-to-promote", "Promote a specific release to public immediately").Required().String() 62 | releaseToPromoteDryRun := releasePromote.Flag("dry-run", "Announce what would be done without doing it").Bool() 63 | 64 | releaseBroken := release.Command("broken", "Mark a release as broken") 65 | releaseBrokenVersion := releaseBroken.Arg("version", "Mark a release as broken").Required().String() 66 | 67 | smoketest := app.Command("smoketest", "Set the smoke testing status of a build") 68 | smoketestBuildA := smoketest.Flag("build-a", "The first of the two IDs comprising the new build").Required().String() 69 | smoketestPlatform := smoketest.Flag("platform", "The build's platform (darwin, linux, windows)").Required().String() 70 | smoketestEnable := smoketest.Flag("enable", "Whether smoketesting should be enabled").Required().Bool() 71 | smoketestMaxTesters := smoketest.Flag("max-testers", "Max number of testers for this build").Required().Int() 72 | 73 | dumplogCmd := app.Command("dumplog", "Show the log file") 74 | dumplogCommandLabel := dumplogCmd.Arg("label", "Launchd job label").Required().String() 75 | 76 | gitDiffCmd := app.Command("gdiff", "Show the git diff") 77 | gitDiffRepo := gitDiffCmd.Arg("repo", "Repo path relative to $GOPATH/src").Required().String() 78 | 79 | gitCleanCmd := app.Command("gclean", "Clean the repos go/go-ios/go-android") 80 | nodeModuleCleanCmd := app.Command("nodeModuleClean", "Clean the ios/android node_modules") 81 | 82 | upgrade := app.Command("upgrade", "Upgrade package") 83 | upgradePackageName := upgrade.Arg("name", "Package name (yarn, go, fastlane, etc)").Required().String() 84 | 85 | cmd, usage, cmdErr := cli.Parse(app, args, stringBuffer) 86 | if usage != "" || cmdErr != nil { 87 | return usage, cmdErr 88 | } 89 | 90 | home := os.Getenv("HOME") 91 | path := "/sbin:/usr/sbin:/bin:/usr/local/bin:/usr/bin:/opt/homebrew/bin" 92 | env := launchd.NewEnv(home, path) 93 | androidHome := "/usr/local/opt/android-sdk" 94 | ndkVer := "23.1.7779620" 95 | NDKPath := "/Users/build/Library/Android/sdk/ndk/" + ndkVer 96 | 97 | switch cmd { 98 | case cancel.FullCommand(): 99 | if *cancelLabel == "" { 100 | return "Label required for cancel", errors.New("Label required for cancel") 101 | } 102 | return launchd.Stop(*cancelLabel) 103 | 104 | case buildDarwin.FullCommand(): 105 | smokeTest := true 106 | skipCI := *buildDarwinSkipCI 107 | testBuild := *buildDarwinTest 108 | // If it's a custom build, make it a test build unless --smoke is passed. 109 | if *buildDarwinClientCommit != "" || *buildDarwinKbfsCommit != "" { 110 | smokeTest = *buildDarwinSmoke 111 | testBuild = !*buildDarwinSmoke 112 | } 113 | script := launchd.Script{ 114 | Label: "keybase.build.darwin", 115 | Path: "github.com/keybase/client/packaging/build_darwin.sh", 116 | BucketName: "prerelease.keybase.io", 117 | Platform: "darwin", 118 | EnvVars: []launchd.EnvVar{ 119 | {Key: "SMOKE_TEST", Value: boolToEnvString(smokeTest)}, 120 | {Key: "TEST", Value: boolToEnvString(testBuild)}, 121 | {Key: "CLIENT_COMMIT", Value: *buildDarwinClientCommit}, 122 | {Key: "KBFS_COMMIT", Value: *buildDarwinKbfsCommit}, 123 | // TODO: Rename to SKIP_CI in packaging scripts 124 | {Key: "NOWAIT", Value: boolToEnvString(skipCI)}, 125 | {Key: "NOPULL", Value: boolToEnvString(*buildDarwinNoPull)}, 126 | {Key: "NOS3", Value: boolToEnvString(*buildDarwinNoS3)}, 127 | {Key: "NONOTARIZE", Value: boolToEnvString(*buildDarwinNoNotarize)}, 128 | }, 129 | } 130 | return runScript(bot, channel, env, script) 131 | 132 | case buildMobile.FullCommand(): 133 | skipCI := *buildMobileSkipCI 134 | automated := *buildMobileAutomated 135 | script := launchd.Script{ 136 | Label: "keybase.build.mobile", 137 | Path: "github.com/keybase/client/packaging/build_mobile.sh", 138 | BucketName: "prerelease.keybase.io", 139 | EnvVars: []launchd.EnvVar{ 140 | {Key: "ANDROID_HOME", Value: androidHome}, 141 | {Key: "ANDROID_SDK", Value: androidHome}, 142 | {Key: "ANDROID_SDK_ROOT", Value: androidHome}, 143 | {Key: "ANDROID_NDK_HOME", Value: NDKPath}, 144 | {Key: "NDK_HOME", Value: NDKPath}, 145 | {Key: "ANDROID_NDK", Value: NDKPath}, 146 | {Key: "CLIENT_COMMIT", Value: *buildMobileCientCommit}, 147 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)}, 148 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)}, 149 | }, 150 | } 151 | env.GoPath = env.PathFromHome("go-ios") 152 | return runScript(bot, channel, env, script) 153 | 154 | case buildAndroid.FullCommand(): 155 | skipCI := *buildAndroidSkipCI 156 | automated := *buildAndroidAutomated 157 | script := launchd.Script{ 158 | Label: "keybase.build.android", 159 | Path: "github.com/keybase/client/packaging/android/build_and_publish.sh", 160 | BucketName: "prerelease.keybase.io", 161 | EnvVars: []launchd.EnvVar{ 162 | {Key: "ANDROID_HOME", Value: androidHome}, 163 | {Key: "ANDROID_NDK_HOME", Value: NDKPath}, 164 | {Key: "ANDROID_NDK", Value: NDKPath}, 165 | {Key: "CLIENT_COMMIT", Value: *buildAndroidCientCommit}, 166 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)}, 167 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)}, 168 | }, 169 | } 170 | env.GoPath = env.PathFromHome("go-android") // Custom go path for Android so we don't conflict 171 | return runScript(bot, channel, env, script) 172 | 173 | case buildIOS.FullCommand(): 174 | skipCI := *buildIOSSkipCI 175 | iosClean := *buildIOSClean 176 | automated := *buildIOSAutomated 177 | script := launchd.Script{ 178 | Label: "keybase.build.ios", 179 | Path: "github.com/keybase/client/packaging/ios/build_and_publish.sh", 180 | BucketName: "prerelease.keybase.io", 181 | EnvVars: []launchd.EnvVar{ 182 | {Key: "CLIENT_COMMIT", Value: *buildIOSCientCommit}, 183 | {Key: "CLEAN", Value: boolToEnvString(iosClean)}, 184 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)}, 185 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)}, 186 | }, 187 | } 188 | env.GoPath = env.PathFromHome("go-ios") // Custom go path for iOS so we don't conflict 189 | return runScript(bot, channel, env, script) 190 | 191 | case releasePromote.FullCommand(): 192 | script := launchd.Script{ 193 | Label: "keybase.release.promote", 194 | Path: "github.com/keybase/slackbot/scripts/release.promote.sh", 195 | BucketName: "prerelease.keybase.io", 196 | Platform: *releaseToPromotePlatform, 197 | EnvVars: []launchd.EnvVar{ 198 | {Key: "RELEASE_TO_PROMOTE", Value: *releaseToPromote}, 199 | {Key: "DRY_RUN", Value: boolToString(*releaseToPromoteDryRun)}, 200 | }, 201 | } 202 | return runScript(bot, channel, env, script) 203 | 204 | case dumplogCmd.FullCommand(): 205 | readPath, err := env.LogPathForLaunchdLabel(*dumplogCommandLabel) 206 | if err != nil { 207 | return "", err 208 | } 209 | script := launchd.Script{ 210 | Label: "keybase.dumplog", 211 | Path: "github.com/keybase/slackbot/scripts/dumplog.sh", 212 | BucketName: "prerelease.keybase.io", 213 | EnvVars: []launchd.EnvVar{ 214 | {Key: "READ_PATH", Value: readPath}, 215 | {Key: "NOLOG", Value: boolToEnvString(true)}, 216 | }, 217 | } 218 | return runScript(bot, channel, env, script) 219 | 220 | case gitDiffCmd.FullCommand(): 221 | rawRepoText := *gitDiffRepo 222 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1] 223 | 224 | script := launchd.Script{ 225 | Label: "keybase.gitdiff", 226 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh", 227 | BucketName: "prerelease.keybase.io", 228 | EnvVars: []launchd.EnvVar{ 229 | {Key: "REPO", Value: repoParsed}, 230 | {Key: "PREFIX_GOPATH", Value: boolToEnvString(true)}, 231 | {Key: "SCRIPT_TO_RUN", Value: "./git_diff.sh"}, 232 | }, 233 | } 234 | return runScript(bot, channel, env, script) 235 | 236 | case gitCleanCmd.FullCommand(): 237 | script := launchd.Script{ 238 | Label: "keybase.gitclean", 239 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh", 240 | BucketName: "prerelease.keybase.io", 241 | EnvVars: []launchd.EnvVar{ 242 | {Key: "SCRIPT_TO_RUN", Value: "./git_clean.sh"}, 243 | }, 244 | } 245 | return runScript(bot, channel, env, script) 246 | 247 | case nodeModuleCleanCmd.FullCommand(): 248 | script := launchd.Script{ 249 | Label: "keybase.nodeModuleClean", 250 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh", 251 | BucketName: "prerelease.keybase.io", 252 | EnvVars: []launchd.EnvVar{ 253 | {Key: "SCRIPT_TO_RUN", Value: "./node_module_clean.sh"}, 254 | }, 255 | } 256 | return runScript(bot, channel, env, script) 257 | 258 | case releaseBroken.FullCommand(): 259 | script := launchd.Script{ 260 | Label: "keybase.release.broken", 261 | Path: "github.com/keybase/slackbot/scripts/release.broken.sh", 262 | BucketName: "prerelease.keybase.io", 263 | Platform: "darwin", 264 | EnvVars: []launchd.EnvVar{ 265 | {Key: "BROKEN_RELEASE", Value: *releaseBrokenVersion}, 266 | }, 267 | } 268 | return runScript(bot, channel, env, script) 269 | 270 | case smoketest.FullCommand(): 271 | script := launchd.Script{ 272 | Label: "keybase.smoketest", 273 | Path: "github.com/keybase/slackbot/scripts/smoketest.sh", 274 | BucketName: "prerelease.keybase.io", 275 | Platform: *smoketestPlatform, 276 | EnvVars: []launchd.EnvVar{ 277 | {Key: "SMOKETEST_BUILD_A", Value: *smoketestBuildA}, 278 | {Key: "SMOKETEST_MAX_TESTERS", Value: strconv.Itoa(*smoketestMaxTesters)}, 279 | {Key: "SMOKETEST_ENABLE", Value: boolToString(*smoketestEnable)}, 280 | }, 281 | } 282 | return runScript(bot, channel, env, script) 283 | 284 | case upgrade.FullCommand(): 285 | script := launchd.Script{ 286 | Label: "keybase.update", 287 | Path: "github.com/keybase/slackbot/scripts/upgrade.sh", 288 | EnvVars: []launchd.EnvVar{ 289 | {Key: "NAME", Value: *upgradePackageName}, 290 | }, 291 | } 292 | return runScript(bot, channel, env, script) 293 | } 294 | 295 | return cmd, nil 296 | } 297 | 298 | func (k *keybot) Help(bot *slackbot.Bot) string { 299 | out, err := k.Run(bot, "", nil) 300 | if err != nil { 301 | return fmt.Sprintf("Error getting help: %s", err) 302 | } 303 | return out 304 | } 305 | -------------------------------------------------------------------------------- /keybot/keybot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd $dir 7 | 8 | git pull --ff-only 9 | go install github.com/keybase/slackbot/keybot 10 | "$GOPATH/bin/keybot" 11 | -------------------------------------------------------------------------------- /keybot/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "runtime" 11 | 12 | "github.com/keybase/go-keybase-chat-bot/kbchat" 13 | 14 | "github.com/keybase/slackbot" 15 | "github.com/keybase/slackbot/launchd" 16 | ) 17 | 18 | func boolToString(b bool) string { 19 | if b { 20 | return "true" 21 | } 22 | return "false" 23 | } 24 | 25 | func boolToEnvString(b bool) string { 26 | if b { 27 | return "1" 28 | } 29 | return "0" 30 | } 31 | 32 | func runScript(bot *slackbot.Bot, channel string, env launchd.Env, script launchd.Script) (string, error) { 33 | if bot.Config().DryRun() { 34 | return fmt.Sprintf("I would have run a launchd job (%s)\nPath: %#v\nEnvVars: %#v", script.Label, script.Path, script.EnvVars), nil 35 | } 36 | 37 | if bot.Config().Paused() { 38 | return fmt.Sprintf("I'm paused so I can't do that, but I would have run a launchd job (%s)", script.Label), nil 39 | } 40 | 41 | // Write job plist 42 | path, err := env.WritePlist(script) 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | // Remove previous log 48 | if err := launchd.CleanupLog(env, script.Label); err != nil { 49 | return "", err 50 | } 51 | 52 | msg := fmt.Sprintf("I'm starting the job `%s`. To cancel run `!%s cancel %s`", script.Label, bot.Name(), script.Label) 53 | bot.SendMessage(msg, channel) 54 | return launchd.NewStartCommand(path, script.Label).Run("", nil) 55 | } 56 | 57 | func addBasicCommands(bot *slackbot.Bot) { 58 | bot.AddCommand("date", slackbot.NewExecCommand("/bin/date", nil, true, "Show the current date", bot.Config())) 59 | bot.AddCommand("pause", slackbot.NewPauseCommand(bot.Config())) 60 | bot.AddCommand("resume", slackbot.NewResumeCommand(bot.Config())) 61 | bot.AddCommand("config", slackbot.NewShowConfigCommand(bot.Config())) 62 | bot.AddCommand("toggle-dryrun", slackbot.NewToggleDryRunCommand(bot.Config())) 63 | if runtime.GOOS != "windows" { 64 | bot.AddCommand("restart", slackbot.NewExecCommand("/bin/launchctl", []string{"stop", bot.Label()}, false, "Restart the bot", bot.Config())) 65 | } 66 | } 67 | 68 | type extension interface { 69 | Run(b *slackbot.Bot, channel string, args []string) (string, error) 70 | Help(bot *slackbot.Bot) string 71 | } 72 | 73 | func main() { 74 | name := os.Getenv("BOT_NAME") 75 | var err error 76 | var label string 77 | var ext extension 78 | var backend slackbot.BotBackend 79 | var hybrids []slackbot.HybridBackendMember 80 | var channel string 81 | 82 | // Set up Slack 83 | slackChannel := os.Getenv("SLACK_CHANNEL") 84 | slackBackend, err := slackbot.NewSlackBotBackend(slackbot.GetTokenFromEnv()) 85 | if err != nil { 86 | log.Printf("failed to initialize Slack backend: %s", err) 87 | } else { 88 | hybrids = append(hybrids, slackbot.HybridBackendMember{ 89 | Backend: slackBackend, 90 | Channel: slackChannel, 91 | }) 92 | } 93 | 94 | // Set up Keybase 95 | var opts kbchat.RunOptions 96 | keybaseChannel := os.Getenv("KEYBASE_CHAT_CONVID") 97 | opts.KeybaseLocation = os.Getenv("KEYBASE_LOCATION") 98 | opts.HomeDir = os.Getenv("KEYBASE_HOME") 99 | oneshotUsername := os.Getenv("KEYBASE_ONESHOT_USERNAME") 100 | oneshotPaperkey := os.Getenv("KEYBASE_ONESHOT_PAPERKEY") 101 | if len(oneshotPaperkey) > 0 && len(oneshotUsername) > 0 { 102 | opts.Oneshot = &kbchat.OneshotOptions{ 103 | Username: oneshotUsername, 104 | PaperKey: oneshotPaperkey, 105 | } 106 | } 107 | keybaseBackend, err := slackbot.NewKeybaseChatBotBackend(name, keybaseChannel, opts) 108 | if err != nil { 109 | log.Printf("failed to initialize Keybase backend: %s", err) 110 | } else { 111 | hybrids = append(hybrids, slackbot.HybridBackendMember{ 112 | Backend: keybaseBackend, 113 | Channel: keybaseChannel, 114 | }) 115 | } 116 | 117 | // Set up hybrid backend 118 | hybridChannel := "" 119 | hybridBackend := slackbot.NewHybridBackend(hybrids...) 120 | 121 | switch name { 122 | case "keybot": 123 | ext = &keybot{} 124 | label = "keybase.keybot" 125 | backend = hybridBackend 126 | channel = hybridChannel 127 | case "winbot": 128 | ext = &winbot{} 129 | label = "keybase.winbot" 130 | channel = hybridChannel 131 | backend = hybridBackend 132 | default: 133 | log.Fatal("Invalid BOT_NAME") 134 | } 135 | 136 | bot := slackbot.NewBot(slackbot.ReadConfigOrDefault(), name, label, backend) 137 | addBasicCommands(bot) 138 | 139 | // Extension 140 | runFn := func(channel string, args []string) (string, error) { 141 | return ext.Run(bot, channel, args) 142 | } 143 | bot.SetDefault(slackbot.NewFuncCommand(runFn, "Extension", bot.Config())) 144 | bot.SetHelp(bot.HelpMessage() + "\n\n" + ext.Help(bot)) 145 | 146 | bot.SendMessage("I'm running.", channel) 147 | 148 | bot.Listen() 149 | } 150 | -------------------------------------------------------------------------------- /keybot/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/keybase/slackbot" 11 | ) 12 | 13 | func TestAddBasicCommands(t *testing.T) { 14 | bot, err := slackbot.NewTestBot() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | addBasicCommands(bot) 19 | } 20 | 21 | func TestPromoteRelease(t *testing.T) { 22 | bot, err := slackbot.NewTestBot() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | ext := &keybot{} 27 | out, err := ext.Run(bot, "", []string{"release", "promote", "darwin", "1.2.3"}) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if out != "I would have run a launchd job (keybase.release.promote)\nPath: \"github.com/keybase/slackbot/scripts/release.promote.sh\"\nEnvVars: []launchd.EnvVar{launchd.EnvVar{Key:\"RELEASE_TO_PROMOTE\", Value:\"1.2.3\"}, launchd.EnvVar{Key:\"DRY_RUN\", Value:\"false\"}}" { 32 | t.Errorf("Unexpected output: %s", out) 33 | } 34 | 35 | out, err = ext.Run(bot, "", []string{"release", "promote", "darwin", "1.2.3", "--dry-run"}) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if out != "I would have run a launchd job (keybase.release.promote)\nPath: \"github.com/keybase/slackbot/scripts/release.promote.sh\"\nEnvVars: []launchd.EnvVar{launchd.EnvVar{Key:\"RELEASE_TO_PROMOTE\", Value:\"1.2.3\"}, launchd.EnvVar{Key:\"DRY_RUN\", Value:\"true\"}}" { 40 | t.Errorf("Unexpected output: %s", out) 41 | } 42 | } 43 | 44 | func TestInvalidUsage(t *testing.T) { 45 | bot, err := slackbot.NewTestBot() 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | ext := &keybot{} 50 | out, err := ext.Run(bot, "", []string{"release", "oops"}) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | if !strings.HasPrefix(out, "```\nI don't know what you mean by") { 55 | t.Errorf("Unexpected output: %s", out) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /keybot/winbot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "path" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/keybase/slackbot" 18 | "github.com/keybase/slackbot/cli" 19 | kingpin "gopkg.in/alecthomas/kingpin.v2" 20 | ) 21 | 22 | type winbot struct { 23 | testAuto chan struct{} 24 | stopAuto chan struct{} 25 | } 26 | 27 | const numLogLines = 10 28 | 29 | // Keep track of the current build process, protected with a mutex, 30 | // to support cancellation 31 | var buildProcessMutex sync.Mutex 32 | var buildProcess *os.Process 33 | 34 | func (d *winbot) Run(bot *slackbot.Bot, channel string, args []string) (string, error) { 35 | app := kingpin.New("winbot", "Job command parser for winbot") 36 | app.Terminate(nil) 37 | stringBuffer := new(bytes.Buffer) 38 | app.Writer(stringBuffer) 39 | 40 | buildWindows := app.Command("build", "Start a windows build") 41 | buildWindowsTest := buildWindows.Flag("test", "Test build, skips admin/test channel").Bool() 42 | buildWindowsCientCommit := buildWindows.Flag("client-commit", "Build a specific client commit").String() 43 | buildWindowsKbfsCommit := buildWindows.Flag("kbfs-commit", "Build a specific kbfs commit").String() 44 | buildWindowsUpdaterCommit := buildWindows.Flag("updater-commit", "Build a specific updater commit").String() 45 | buildWindowsSkipCI := buildWindows.Flag("skip-ci", "Whether to skip CI").Bool() 46 | buildWindowsSmoke := buildWindows.Flag("smoke", "Build a smoke pair").Bool() 47 | buildWindowsDevCert := buildWindows.Flag("dev-cert", "Build using devel code signing cert").Bool() 48 | buildWindowsAuto := buildWindows.Flag("automated", "Specify build was triggered automatically").Hidden().Bool() 49 | 50 | cancel := app.Command("cancel", "Cancel current") 51 | 52 | dumplogCmd := app.Command("dumplog", "Show the last log file") 53 | gitDiffCmd := app.Command("gdiff", "Show the git diff") 54 | gitDiffRepo := gitDiffCmd.Arg("repo", "Repo path relative to $GOPATH/src").Required().String() 55 | 56 | gitCleanCmd := app.Command("gclean", "Clean the repo") 57 | gitCleanRepo := gitCleanCmd.Arg("repo", "Repo path relative to $GOPATH/src").Required().String() 58 | 59 | logFileName := path.Join(os.TempDir(), "keybase.build.windows.log") 60 | 61 | testAutoBuild := app.Command("testauto", "Simulate an automated daily build").Hidden() 62 | startAutoTimer := app.Command("startAutoTimer", "Start the auto build timer") 63 | startAutoTimerInterval := startAutoTimer.Flag("interval", "Number of hours between auto builds, 0 to stop").Default("24").Int() 64 | startAutoTimerStartHour := startAutoTimer.Flag("startHour", "Number of hours after midnight to build, local time").Default("7").Int() 65 | startAutoTimerDelay := startAutoTimer.Flag("delay", "Number of hours to wait before starting auto timer").Default("0").Int() 66 | 67 | restartCmd := app.Command("restart", "Quit and let calling script invoke bot again") 68 | 69 | cmd, usage, cmdErr := cli.Parse(app, args, stringBuffer) 70 | if usage != "" || cmdErr != nil { 71 | return usage, cmdErr 72 | } 73 | 74 | // do these regardless of dry run status 75 | if cmd == testAutoBuild.FullCommand() { 76 | d.testAuto <- struct{}{} 77 | return "Sent test signal", nil 78 | } 79 | 80 | if cmd == startAutoTimer.FullCommand() { 81 | if d.stopAuto != nil { 82 | d.stopAuto <- struct{}{} 83 | } 84 | if *startAutoTimerInterval > 0 { 85 | go d.winAutoBuild(bot, channel, *startAutoTimerInterval, *startAutoTimerDelay, *startAutoTimerStartHour) 86 | } 87 | return "", nil 88 | } 89 | 90 | if bot.Config().DryRun() { 91 | return fmt.Sprintf("I would have run: `%#v`", cmd), nil 92 | } 93 | 94 | switch cmd { 95 | case cancel.FullCommand(): 96 | buildProcessMutex.Lock() 97 | defer buildProcessMutex.Unlock() 98 | if buildProcess == nil { 99 | return "No build running", nil 100 | } 101 | if err := buildProcess.Kill(); err != nil { 102 | return "failed to cancel build", err 103 | } 104 | 105 | case buildWindows.FullCommand(): 106 | smokeTest := *buildWindowsSmoke 107 | skipCI := *buildWindowsSkipCI 108 | skipTestChannel := *buildWindowsTest 109 | devCert := 0 110 | if *buildWindowsDevCert { 111 | devCert = 1 112 | } 113 | var autoBuild string 114 | 115 | if bot.Config().DryRun() { 116 | return "I would have done a build", nil 117 | } 118 | 119 | if bot.Config().Paused() { 120 | return "I'm paused so I can't do that, but I would have done a build", nil 121 | } 122 | 123 | if *buildWindowsAuto { 124 | autoBuild = "Automatic Build: " 125 | } 126 | 127 | // Test channel tells the scripts this is an admin build 128 | updateChannel := "Test" 129 | if skipTestChannel { 130 | if smokeTest { 131 | return "Test and Smoke are exclusive options", nil 132 | } 133 | updateChannel = "None" 134 | } else if smokeTest { 135 | updateChannel = "Smoke" 136 | if !skipCI { 137 | updateChannel = "SmokeCI" 138 | } 139 | } 140 | 141 | msg := fmt.Sprintf(autoBuild+"I'm starting the job `windows build`. To cancel run `!%s cancel`. ", bot.Name()) 142 | msg = fmt.Sprintf(msg+"updateChannel is %s, smokeTest is %v, devCert is %v, logFileName %s", 143 | updateChannel, smokeTest, devCert, logFileName) 144 | bot.SendMessage(msg, channel) 145 | 146 | os.Remove(logFileName) 147 | logf, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE, 0644) 148 | if err != nil { 149 | return "Unable to open logfile", err 150 | } 151 | 152 | gitCmd := exec.Command( 153 | "git.exe", 154 | "checkout", 155 | "master", 156 | ) 157 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 158 | stdoutStderr, err := gitCmd.CombinedOutput() 159 | _, _ = logf.Write(stdoutStderr) 160 | if err != nil { 161 | _, _ = logf.WriteString(gitCmd.Dir) 162 | logf.Close() 163 | return string(stdoutStderr), err 164 | } 165 | 166 | gitCmd = exec.Command( 167 | "git.exe", 168 | "pull", 169 | ) 170 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 171 | stdoutStderr, err = gitCmd.CombinedOutput() 172 | _, _ = logf.Write(stdoutStderr) 173 | if err != nil { 174 | _, _ = logf.WriteString(gitCmd.Dir) 175 | logf.Close() 176 | return string(stdoutStderr), err 177 | } 178 | 179 | if buildWindowsCientCommit != nil && *buildWindowsCientCommit != "" && *buildWindowsCientCommit != "master" { 180 | msg := fmt.Sprintf(autoBuild+"I'm trying to use commit %s", *buildWindowsCientCommit) 181 | bot.SendMessage(msg, channel) 182 | 183 | gitCmd = exec.Command( 184 | "git.exe", 185 | "checkout", 186 | *buildWindowsCientCommit, 187 | ) 188 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 189 | stdoutStderr, err = gitCmd.CombinedOutput() 190 | _, _ = logf.Write(stdoutStderr) 191 | 192 | if err != nil { 193 | _, _ = logf.WriteString(fmt.Sprintf("error doing git pull in %s\n", gitCmd.Dir)) 194 | logf.Close() 195 | return string(stdoutStderr), err 196 | } 197 | 198 | // Test if we're on a branch. If so, do git pull once more. 199 | gitCmd = exec.Command( 200 | "git.exe", 201 | "rev-parse", 202 | "--abbrev-ref", 203 | "HEAD", 204 | ) 205 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 206 | stdoutStderr, err = gitCmd.CombinedOutput() 207 | if err != nil { 208 | _, _ = logf.WriteString(fmt.Sprintf("error going git rev-parse dir: %s\n", gitCmd.Dir)) 209 | logf.Close() 210 | return string(stdoutStderr), err 211 | } 212 | commit := strings.TrimSpace(string(stdoutStderr)) 213 | if commit != "HEAD" { 214 | gitCmd = exec.Command( 215 | "git.exe", 216 | "pull", 217 | ) 218 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 219 | stdoutStderr, err = gitCmd.CombinedOutput() 220 | _, _ = logf.Write(stdoutStderr) 221 | if err != nil { 222 | _, _ = logf.WriteString(fmt.Sprintf("error doing git pull on %s in %s\n", commit, gitCmd.Dir)) 223 | logf.Close() 224 | return string(stdoutStderr), err 225 | } 226 | } 227 | } 228 | 229 | gitCmd = exec.Command( 230 | "git.exe", 231 | "rev-parse", 232 | "HEAD", 233 | ) 234 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client") 235 | stdoutStderr, err = gitCmd.CombinedOutput() 236 | if err != nil { 237 | _, _ = logf.WriteString(fmt.Sprintf("error getting current commit for logs: %s", gitCmd.Dir)) 238 | logf.Close() 239 | return string(stdoutStderr), err 240 | } 241 | _, _ = logf.WriteString(fmt.Sprintf("HEAD is currently at %s\n", string(stdoutStderr))) 242 | 243 | cmd := exec.Command( 244 | "cmd", "/c", 245 | path.Join(os.Getenv("GOPATH"), "src/github.com/keybase/client/packaging/windows/dorelease.cmd"), 246 | ">>", 247 | logFileName, 248 | "2>&1") 249 | cmd.Env = append(os.Environ(), 250 | "ClientRevision="+*buildWindowsCientCommit, 251 | "KbfsRevision="+*buildWindowsKbfsCommit, 252 | "UpdaterRevision="+*buildWindowsUpdaterCommit, 253 | "UpdateChannel="+updateChannel, 254 | fmt.Sprintf("DevCert=%d", devCert), 255 | "SlackBot=1", 256 | ) 257 | _, _ = logf.WriteString(fmt.Sprintf("cmd: %+v\n", cmd)) 258 | logf.Close() 259 | 260 | go func() { 261 | err := cmd.Start() 262 | if err != nil { 263 | bot.SendMessage(fmt.Sprintf("unable to start: %s", err), channel) 264 | } 265 | buildProcessMutex.Lock() 266 | buildProcess = cmd.Process 267 | buildProcessMutex.Unlock() 268 | err = cmd.Wait() 269 | 270 | bucketName := os.Getenv("BUCKET_NAME") 271 | if bucketName == "" { 272 | bucketName = "prerelease.keybase.io" 273 | } 274 | sendLogCmd := exec.Command( 275 | path.Join(os.Getenv("GOPATH"), "src/github.com/keybase/client/go/release/release.exe"), 276 | "save-log", 277 | "--maxsize=5000000", 278 | "--bucket-name="+bucketName, 279 | "--path="+logFileName, 280 | ) 281 | resultMsg := autoBuild + "Finished the job `windows build`" 282 | if err != nil { 283 | resultMsg = autoBuild + "Error in job `windows build`" 284 | var lines [numLogLines]string 285 | // Send a log snippet too 286 | index := 0 287 | lineCount := 0 288 | 289 | f, err := os.Open(logFileName) 290 | if err != nil { 291 | bot.SendMessage(autoBuild+"Error reading "+logFileName+": "+err.Error(), channel) 292 | } 293 | 294 | scanner := bufio.NewScanner(f) 295 | for scanner.Scan() { 296 | lines[lineCount%numLogLines] = scanner.Text() 297 | lineCount++ 298 | } 299 | if err := scanner.Err(); err != nil { 300 | bot.SendMessage(autoBuild+"Error scanning "+logFileName+": "+err.Error(), channel) 301 | } 302 | if lineCount > numLogLines { 303 | index = lineCount % numLogLines 304 | lineCount = numLogLines 305 | } 306 | snippet := "```\n" 307 | for i := 0; i < lineCount; i++ { 308 | snippet += lines[(i+index)%numLogLines] + "\n" 309 | } 310 | snippet += "```" 311 | bot.SendMessage(snippet, channel) 312 | } 313 | urlBytes, err2 := sendLogCmd.Output() 314 | if err2 != nil { 315 | msg := fmt.Sprintf("%s, log upload error %s", resultMsg, err2.Error()) 316 | bot.SendMessage(msg, channel) 317 | } else { 318 | msg := fmt.Sprintf("%s, view log at %s", resultMsg, string(urlBytes)) 319 | bot.SendMessage(msg, channel) 320 | } 321 | }() 322 | return "", nil 323 | case dumplogCmd.FullCommand(): 324 | logContents, err := os.ReadFile(logFileName) 325 | if err != nil { 326 | return "Error reading " + logFileName, err 327 | } 328 | index := 0 329 | if len(logContents) > 1000 { 330 | index = len(logContents) - 1000 331 | } 332 | bot.SendMessage(string(logContents[index:]), channel) 333 | 334 | case gitDiffCmd.FullCommand(): 335 | rawRepoText := *gitDiffRepo 336 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1] 337 | 338 | gitDiffCmd := exec.Command( 339 | "git.exe", 340 | "diff", 341 | ) 342 | gitDiffCmd.Dir = os.ExpandEnv(path.Join("$GOPATH/src", repoParsed)) 343 | 344 | if exists, err := Exists(path.Join(gitDiffCmd.Dir, ".git")); !exists { 345 | return "Not a git repo", err 346 | } 347 | 348 | stdoutStderr, err := gitDiffCmd.CombinedOutput() 349 | if err != nil { 350 | return "Error", err 351 | } 352 | bot.SendMessage(string(stdoutStderr), channel) 353 | 354 | case gitCleanCmd.FullCommand(): 355 | rawRepoText := *gitCleanRepo 356 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1] 357 | 358 | gitCleanCmd := exec.Command( 359 | "git.exe", 360 | "clean", 361 | "-f", 362 | ) 363 | gitCleanCmd.Dir = os.ExpandEnv(path.Join("$GOPATH/src", repoParsed)) 364 | 365 | if exists, err := Exists(path.Join(gitCleanCmd.Dir, ".git")); !exists { 366 | return "Not a git repo", err 367 | } 368 | 369 | stdoutStderr, err := gitCleanCmd.CombinedOutput() 370 | if err != nil { 371 | return "Error", err 372 | } 373 | 374 | bot.SendMessage(string(stdoutStderr), channel) 375 | 376 | case restartCmd.FullCommand(): 377 | os.Exit(0) //nolint 378 | } 379 | return cmd, nil 380 | } 381 | 382 | func (d *winbot) Help(bot *slackbot.Bot) string { 383 | out, err := d.Run(bot, "", nil) 384 | if err != nil { 385 | return fmt.Sprintf("Error getting help: %s", err) 386 | } 387 | return out 388 | } 389 | 390 | func Exists(name string) (bool, error) { 391 | _, err := os.Stat(name) 392 | if os.IsNotExist(err) { 393 | return false, nil 394 | } 395 | return err != nil, err 396 | } 397 | 398 | func (d *winbot) winAutoBuild(bot *slackbot.Bot, channel string, interval int, delay int, startHour int) { 399 | d.testAuto = make(chan struct{}) 400 | d.stopAuto = make(chan struct{}) 401 | for { 402 | hour := time.Now().Hour() + delay 403 | if delay > 0 { 404 | delay = 0 405 | } else { 406 | hour = ((interval - hour) + startHour) 407 | } 408 | next := time.Now().Add(time.Hour * time.Duration(hour)) 409 | for next.Weekday() == time.Saturday || next.Weekday() == time.Sunday { 410 | hour += interval 411 | next = time.Now().Add(time.Hour * time.Duration(hour)) 412 | } 413 | 414 | msg := fmt.Sprintf("Next automatic build at %s", next.Format(time.RFC822)) 415 | bot.SendMessage(msg, channel) 416 | 417 | args := []string{"build", "--automated"} 418 | 419 | select { 420 | case <-d.testAuto: 421 | case <-time.After(time.Duration(hour) * time.Hour): 422 | args = append(args, "--smoke") 423 | case <-d.stopAuto: 424 | return 425 | } 426 | message, err := d.Run(bot, channel, args) 427 | if err != nil { 428 | msg := fmt.Sprintf("AutoBuild ERROR -- %s: %s", message, err.Error()) 429 | bot.SendMessage(msg, channel) 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /launchd/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package launchd 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | // StartCommand loads and starts a launchd job 12 | type StartCommand struct { 13 | plistPath string 14 | label string 15 | } 16 | 17 | // NewStartCommand creates a StartCommand 18 | func NewStartCommand(plistPath string, label string) StartCommand { 19 | return StartCommand{ 20 | plistPath: plistPath, 21 | label: label, 22 | } 23 | } 24 | 25 | // Run runs the exec command 26 | func (c StartCommand) Run(_ string, _ []string) (string, error) { 27 | if _, err := exec.Command("/bin/launchctl", "unload", c.plistPath).CombinedOutput(); err != nil { 28 | return "", fmt.Errorf("Error in launchctl unload: %s", err) 29 | } 30 | 31 | if _, err := exec.Command("/bin/launchctl", "load", c.plistPath).CombinedOutput(); err != nil { 32 | return "", fmt.Errorf("Error in launchctl load: %s", err) 33 | } 34 | 35 | if _, err := exec.Command("/bin/launchctl", "start", c.label).CombinedOutput(); err != nil { 36 | return "", fmt.Errorf("Error in launchctl start: %s", err) 37 | } 38 | 39 | return "", nil 40 | } 41 | 42 | // Stop a launchd job 43 | func Stop(label string) (string, error) { 44 | if _, err := exec.Command("/bin/launchctl", "stop", label).CombinedOutput(); err != nil { 45 | return "", fmt.Errorf("Error in launchctl stop: %s", err) 46 | } 47 | return fmt.Sprintf("I stopped the job `%s`.", label), nil 48 | } 49 | 50 | // ShowResult decides whether to show the results from the exec 51 | func (c StartCommand) ShowResult() bool { 52 | return false 53 | } 54 | 55 | // Description describes the command 56 | func (c StartCommand) Description() string { 57 | return fmt.Sprintf("Run launchd job (%s)", c.label) 58 | } 59 | 60 | // Label returns job label 61 | func (c StartCommand) Label() string { 62 | return c.label 63 | } 64 | -------------------------------------------------------------------------------- /launchd/example/schedule-command.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | keybase.keybot.build.darwin 7 | EnvironmentVariables 8 | 9 | GOPATH 10 | /Users/test/go 11 | SLACK_TOKEN 12 | 13 | SLACK_CHANNEL 14 | 15 | PATH 16 | /sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin 17 | 18 | ProgramArguments 19 | 20 | /bin/bash 21 | /Users/test/go/src/github.com/keybase/slackbot/scripts/send.sh 22 | !keybot build darwin 23 | 24 | StartCalendarInterval 25 | 26 | 27 | Weekday 28 | 1 29 | Hour 30 | 4 31 | 32 | 33 | Weekday 34 | 1 35 | Hour 36 | 12 37 | 38 | 39 | Weekday 40 | 2 41 | Hour 42 | 4 43 | 44 | 45 | Weekday 46 | 2 47 | Hour 48 | 12 49 | 50 | 51 | Weekday 52 | 3 53 | Hour 54 | 4 55 | 56 | 57 | Weekday 58 | 3 59 | Hour 60 | 12 61 | 62 | 63 | Weekday 64 | 4 65 | Hour 66 | 4 67 | 68 | 69 | Weekday 70 | 4 71 | Hour 72 | 12 73 | 74 | 75 | Weekday 76 | 5 77 | Hour 78 | 4 79 | 80 | 81 | Weekday 82 | 5 83 | Hour 84 | 12 85 | 86 | 87 | StandardErrorPath 88 | /Users/test/Library/Logs/keybase.keybot.build.darwin.log 89 | StandardOutPath 90 | /Users/test/Library/Logs/keybase.keybot.build.darwin.log 91 | 92 | 93 | -------------------------------------------------------------------------------- /launchd/plist.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package launchd 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | // Env is environment for launchd 17 | type Env struct { 18 | Path string 19 | Home string 20 | GoPath string 21 | GoPathForBot string 22 | GithubToken string 23 | SlackToken string 24 | SlackChannel string 25 | AWSAccessKey string 26 | AWSSecretKey string 27 | KeybaseToken string 28 | KeybaseChatConvID string 29 | KeybaseLocation string 30 | KeybaseHome string 31 | } 32 | 33 | // Script is what to run 34 | type Script struct { 35 | Label string 36 | Path string 37 | BucketName string 38 | Platform string 39 | LogPath string 40 | EnvVars []EnvVar 41 | } 42 | 43 | // EnvVar is custom env vars 44 | type EnvVar struct { 45 | Key string 46 | Value string 47 | } 48 | 49 | type job struct { 50 | Env Env 51 | Script Script 52 | LogPath string 53 | } 54 | 55 | const plistTemplate = ` 56 | 57 | 58 | 59 | Label 60 | {{.Script.Label }} 61 | EnvironmentVariables 62 | 63 | GOPATH 64 | {{ .Env.GoPath }} 65 | GITHUB_TOKEN 66 | {{ .Env.GithubToken }} 67 | SLACK_TOKEN 68 | {{ .Env.SlackToken }} 69 | SLACK_CHANNEL 70 | {{ .Env.SlackChannel }} 71 | AWS_ACCESS_KEY 72 | {{ .Env.AWSAccessKey }} 73 | AWS_SECRET_KEY 74 | {{ .Env.AWSSecretKey }} 75 | KEYBASE_TOKEN 76 | {{ .Env.KeybaseToken }} 77 | KEYBASE_CHAT_CONVID 78 | {{ .Env.KeybaseChatConvID }} 79 | KEYBASE_LOCATION 80 | {{ .Env.KeybaseLocation }} 81 | KEYBASE_HOME 82 | {{ .Env.KeybaseHome }} 83 | KEYBASE_RUN_MODE 84 | prod 85 | LANG 86 | en_US.UTF-8 87 | LANGUAGE 88 | en_US.UTF-8 89 | LC_ALL 90 | en_US.UTF-8 91 | PATH 92 | {{ .Env.Path }} 93 | LOG_PATH 94 | {{ .Env.Home }}/Library/Logs/{{ .Script.Label }}.log 95 | BUCKET_NAME 96 | {{ .Script.BucketName }} 97 | SCRIPT_PATH 98 | {{ .Env.GoPath }}/src/{{ .Script.Path }} 99 | PLATFORM 100 | {{ .Script.Platform }} 101 | LABEL 102 | {{ .Script.Label }} 103 | {{ with .Script.EnvVars }}{{ range . }} 104 | {{ .Key }} 105 | {{ .Value }} 106 | {{ end }}{{ end }} 107 | 108 | ProgramArguments 109 | 110 | /bin/bash 111 | {{ .Env.GoPathForBot }}/src/github.com/keybase/slackbot/scripts/run.sh 112 | 113 | StandardErrorPath 114 | {{ .LogPath }} 115 | StandardOutPath 116 | {{ .LogPath }} 117 | 118 | 119 | ` 120 | 121 | // NewEnv creates environment 122 | func NewEnv(home string, path string) Env { 123 | return Env{ 124 | Path: path, 125 | Home: home, 126 | GoPath: os.Getenv("GOPATH"), 127 | GoPathForBot: os.Getenv("GOPATH"), 128 | GithubToken: os.Getenv("GITHUB_TOKEN"), 129 | SlackToken: os.Getenv("SLACK_TOKEN"), 130 | SlackChannel: os.Getenv("SLACK_CHANNEL"), 131 | AWSAccessKey: os.Getenv("AWS_ACCESS_KEY"), 132 | AWSSecretKey: os.Getenv("AWS_SECRET_KEY"), 133 | KeybaseToken: os.Getenv("KEYBASE_TOKEN"), 134 | KeybaseChatConvID: os.Getenv("KEYBASE_CHAT_CONVID"), 135 | KeybaseHome: os.Getenv("KEYBASE_HOME"), 136 | KeybaseLocation: os.Getenv("KEYBASE_LOCATION"), 137 | } 138 | } 139 | 140 | // PathFromHome returns path from home dir for env 141 | func (e Env) PathFromHome(path string) string { 142 | return filepath.Join(os.Getenv("HOME"), path) 143 | } 144 | 145 | // LogPathForLaunchdLabel returns path to log for label 146 | func (e Env) LogPathForLaunchdLabel(label string) (string, error) { 147 | if strings.Contains(label, "..") || strings.Contains(label, "/") || strings.Contains(label, `\`) { 148 | return "", fmt.Errorf("Invalid label") 149 | } 150 | return filepath.Join(e.Home, "Library/Logs", label+".log"), nil 151 | } 152 | 153 | // Plist is plist for env and args 154 | func (e Env) Plist(script Script) ([]byte, error) { 155 | t := template.New("Plist template") 156 | logPath, lerr := e.LogPathForLaunchdLabel(script.Label) 157 | if lerr != nil { 158 | return nil, lerr 159 | } 160 | j := job{Env: e, Script: script, LogPath: logPath} 161 | t, err := t.Parse(plistTemplate) 162 | if err != nil { 163 | return nil, err 164 | } 165 | buff := bytes.NewBufferString("") 166 | err = t.Execute(buff, j) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return buff.Bytes(), nil 171 | } 172 | 173 | // WritePlist writes out plist and returns path that was written to 174 | func (e Env) WritePlist(script Script) (string, error) { 175 | data, err := e.Plist(script) 176 | if err != nil { 177 | return "", err 178 | } 179 | plistDir := e.Home + "/Library/LaunchAgents" 180 | if err := os.MkdirAll(plistDir, 0755); err != nil { 181 | return "", err 182 | } 183 | path := fmt.Sprintf("%s/%s.plist", plistDir, script.Label) 184 | log.Printf("Writing %s", path) 185 | if err := os.WriteFile(path, data, 0755); err != nil { 186 | return "", err 187 | } 188 | return path, nil 189 | } 190 | 191 | // Cleanup removes any files generated by Env 192 | func (e Env) Cleanup(script Script) error { 193 | plistDir := e.Home + "/Library/LaunchAgents" 194 | path := fmt.Sprintf("%s/%s.plist", plistDir, script.Label) 195 | log.Printf("Removing %s", path) 196 | return os.Remove(path) 197 | } 198 | 199 | // CleanupLog removes log path 200 | func CleanupLog(env Env, label string) error { 201 | // Remove log 202 | logPath, lerr := env.LogPathForLaunchdLabel(label) 203 | if lerr != nil { 204 | return lerr 205 | } 206 | if _, err := os.Stat(logPath); err == nil { 207 | if err := os.Remove(logPath); err != nil { 208 | return err 209 | } 210 | } 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /launchd/plist_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package launchd 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestPlist(t *testing.T) { 12 | env := NewEnv(os.Getenv("HOME"), "/usr/bin") 13 | data, err := env.Plist(Script{Label: "test.label", Path: "foo.sh", EnvVars: []EnvVar{{Key: "TEST", Value: "val"}}}) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | t.Logf("Plist: %s", string(data)) 18 | } 19 | -------------------------------------------------------------------------------- /scripts/check_status_and_pull.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # There are lots of places where we need to check stuff like: 4 | # 1) Does repo X exist? 5 | # 2) Does it have master checked out? 6 | # 3) It is clean? 7 | # 4) Is is up to date? 8 | # This script takes care of all that. 9 | 10 | set -e -u -o pipefail 11 | 12 | repo="${1:-}" 13 | if [ -z "$repo" ] ; then 14 | echo "check_status_and_pull.sh needs a repo argument." 15 | exit 1 16 | fi 17 | 18 | if [ ! -d "$repo" ] ; then 19 | echo "Repo directory '$repo' does not exist." 20 | exit 1 21 | fi 22 | 23 | cd "$repo" 24 | 25 | if [ ! -d ".git" ] ; then 26 | # This intentionally doesn't support bare repos. Some callers are going to 27 | # want to mess with the working copy. 28 | echo "Directory '$repo' is not a git repo." 29 | exit 1 30 | fi 31 | 32 | current_branch="$(git symbolic-ref --short HEAD)" 33 | if [ "$current_branch" != "master" ] ; then 34 | echo "Repo '$repo' doesn't have master checked out." 35 | exit 1 36 | fi 37 | 38 | current_status="$(git status --porcelain)" 39 | if [ -n "$current_status" ] ; then 40 | echo "Repo '$repo' isn't clean." 41 | exit 1 42 | fi 43 | 44 | unpushed_commits="$(git log origin/master..master)" 45 | if [ -n "$unpushed_commits" ] ; then 46 | echo "Repo '$repo' has unpushed commits." 47 | exit 1 48 | fi 49 | 50 | echo "Repo '$repo' looks good. Pulling..." 51 | git pull --ff-only 52 | -------------------------------------------------------------------------------- /scripts/dumplog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 6 | cd "$dir" 7 | 8 | client_dir="$dir/../../client" 9 | echo "Loading release tool" 10 | ( 11 | cd "$client_dir/go/buildtools" 12 | go install "github.com/keybase/client/go/release" 13 | ) 14 | release_bin="$GOPATH/bin/release" 15 | 16 | url=$("$release_bin" save-log --maxsize=5000000 --bucket-name=$BUCKET_NAME --path="$READ_PATH") 17 | "$dir/send.sh" "Log saved to $url" 18 | -------------------------------------------------------------------------------- /scripts/git_clean.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script cleans the go path, go-ios path, go-android path 4 | set -e -u -o pipefail 5 | 6 | cd "$GOPATH/src/github.com/keybase/client" 7 | echo $(git fetch) 8 | echo $(git clean -df) 9 | echo $(git reset --hard) 10 | echo $(git checkout master) 11 | echo $(git pull) 12 | 13 | cd "$GOPATH/../go-ios/src/github.com/keybase/client" 14 | echo $(git fetch) 15 | echo $(git clean -df) 16 | echo $(git reset --hard) 17 | echo $(git checkout master) 18 | echo $(git pull) 19 | 20 | cd "$GOPATH/../go-android/src/github.com/keybase/client" 21 | echo $(git fetch) 22 | echo $(git clean -df) 23 | echo $(git reset --hard) 24 | echo $(git checkout master) 25 | echo $(git pull) 26 | -------------------------------------------------------------------------------- /scripts/git_diff.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script answers 4 | # 1) Does repo X exist? 5 | # 2) What's the diff? 6 | 7 | set -e -u -o pipefail 8 | 9 | addGOPATHPrefix="${PREFIX_GOPATH:-}" 10 | repo="${REPO:-}" 11 | 12 | if [ -z "$repo" ] ; then 13 | echo "git_diff.sh needs a repo argument." 14 | exit 1 15 | fi 16 | 17 | if [ -n "$addGOPATHPrefix" ] ; then 18 | repo="$GOPATH/src/$repo" 19 | fi 20 | 21 | if [ ! -d "$repo" ] ; then 22 | echo "Repo directory '$repo' does not exist." 23 | exit 1 24 | fi 25 | 26 | cd "$repo" 27 | 28 | if [ ! -d ".git" ] ; then 29 | # This intentionally doesn't support bare repos. Some callers are going to 30 | # want to mess with the working copy. 31 | echo "Directory '$repo' is not a git repo." 32 | exit 1 33 | fi 34 | 35 | current_status="$(git status --porcelain)" 36 | if [ -n "$current_status" ] ; then 37 | echo "Repo '$repo' isn't clean." 38 | echo "$current_status" 39 | git diff 40 | else 41 | echo "Repo '$repo' is clean." 42 | fi 43 | -------------------------------------------------------------------------------- /scripts/node_module_clean.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | # This script cleans the go-ios node_modules 4 | set -e -u -o pipefail 5 | 6 | rm -rf "$GOPATH/../go-ios/src/github.com/keybase/client/shared/node_modules" 7 | rm -rf "$GOPATH/../go-android/src/github.com/keybase/client/shared/node_modules" 8 | cd "$GOPATH/../go-android/src/github.com/keybase/client/shared" 9 | yarn rn-packager-wipe-cache 10 | yarn cache clean 11 | -------------------------------------------------------------------------------- /scripts/release.broken.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | client_dir="$dir/../../client" 9 | echo "Loading release tool" 10 | (cd "$client_dir/go/buildtools"; go install "github.com/keybase/client/go/release") 11 | release_bin="$GOPATH/bin/release" 12 | 13 | "$release_bin" broken-release --release="$BROKEN_RELEASE" --bucket-name="$BUCKET_NAME" --platform="$PLATFORM" 14 | "$dir/send.sh" "Removed $BROKEN_RELEASE for $PLATFORM ($BUCKET_NAME)" 15 | -------------------------------------------------------------------------------- /scripts/release.promote.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | client_dir="$dir/../../client" 9 | echo "Loading release tool" 10 | (cd "$client_dir/go/buildtools"; go install "github.com/keybase/client/go/release") 11 | release_bin="$GOPATH/bin/release" 12 | 13 | dryrun="" 14 | if [ $DRY_RUN == 'true' ]; then 15 | dryrun="--dry-run" 16 | fi 17 | 18 | if [ -n "$RELEASE_TO_PROMOTE" ]; then 19 | "$release_bin" promote-a-release --release="$RELEASE_TO_PROMOTE" --bucket-name="$BUCKET_NAME" --platform="$PLATFORM" $dryrun 20 | "$dir/send.sh" "Promoted $PLATFORM release $RELEASE_TO_PROMOTE ($BUCKET_NAME)" 21 | else 22 | if [ $DRY_RUN == 'true' ]; then 23 | "$dir/send.sh" "Can't dry-run without a specific release to promote" 24 | exit 1 25 | fi 26 | "$release_bin" promote-releases --bucket-name="$BUCKET_NAME" --platform="$PLATFORM" 27 | "$dir/send.sh" "Promoted $PLATFORM release on ($BUCKET_NAME)" 28 | fi 29 | -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | client_dir="$dir/../../client" 9 | logpath=${LOG_PATH:-} 10 | label=${LABEL:-} 11 | nolog=${NOLOG:-""} # Don't show log at end of job 12 | bucket_name=${BUCKET_NAME:-"prerelease.keybase.io"} 13 | : ${SCRIPT_PATH:?"Need to set SCRIPT_PATH to run script"} 14 | 15 | echo "Loading release tool" 16 | (cd "$client_dir/go/buildtools"; go install "github.com/keybase/client/go/release") 17 | release_bin="$GOPATH/bin/release" 18 | 19 | err_report() { 20 | url=`$release_bin save-log --bucket-name=$bucket_name --path=$logpath --noerr` 21 | "$dir/send.sh" "Error \`$label\`, see $url" 22 | } 23 | 24 | trap 'err_report $LINENO' ERR 25 | 26 | "$SCRIPT_PATH" 27 | 28 | if [ "$nolog" = "" ]; then 29 | url=`$release_bin save-log --bucket-name=$bucket_name --path=$logpath --noerr` 30 | "$dir/send.sh" "Finished \`$label\`, view log at $url" 31 | fi 32 | -------------------------------------------------------------------------------- /scripts/run_and_send_stdout.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | script="${SCRIPT_TO_RUN:-}" 9 | if [ -z "$script" ] ; then 10 | echo "run_and_send_stdout needs a script argument." 11 | exit 1 12 | fi 13 | 14 | result=$($script) 15 | 16 | "$dir/send.sh" "\`$script\`:\`\`\`$result\`\`\`" 17 | -------------------------------------------------------------------------------- /scripts/send.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | # send to keybase chat if we have it in the environment 9 | convid=${KEYBASE_CHAT_CONVID:-} 10 | if [ -n "$convid" ]; then 11 | echo "Sending to Keybase convID: $convid" 12 | location=${KEYBASE_LOCATION:-"keybase"} 13 | home=${KEYBASE_HOME:-$HOME} 14 | $location --home $home chat api -m "{\"method\":\"send\", \"params\": {\"options\": { \"conversation_id\": \"$convid\" , \"message\": { \"body\": \"$@\" }}}}" 15 | fi 16 | -------------------------------------------------------------------------------- /scripts/smoketest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | client_dir="$dir/../../client" 9 | echo "Loading release tool" 10 | (cd "$client_dir/go/buildtools"; go install "github.com/keybase/client/go/release") 11 | release_bin="$GOPATH/bin/release" 12 | 13 | "$release_bin" set-build-in-testing --build-a="$SMOKETEST_BUILD_A" --platform="$PLATFORM" --enable="$SMOKETEST_ENABLE" --max-testers="$SMOKETEST_MAX_TESTERS" 14 | "$dir/send.sh" "Successfully set enable to $SMOKETEST_ENABLE for release $SMOKETEST_BUILD_A." 15 | -------------------------------------------------------------------------------- /scripts/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | name=${NAME:-} 9 | 10 | # NAME comes from the slack command, so it's a good idea to completely 11 | # whitelist the package names here. 12 | if [ "$name" = "go" ]; then 13 | brew upgrade go 14 | elif [ "$name" = "yarn" ]; then 15 | brew upgrade yarn 16 | elif [ "$name" = "cocoapods" ]; then 17 | brew upgrade cocoapods 18 | elif [ "$name" = "fastlane" ]; then 19 | which ruby 20 | ruby --version 21 | which gem 22 | gem --version 23 | gem update fastlane 24 | gem cleanup 25 | fi 26 | -------------------------------------------------------------------------------- /send/README.md: -------------------------------------------------------------------------------- 1 | ## Send 2 | 3 | Script and go command for sending a message to a slack channel. 4 | 5 | This is called from bash scripts to show progress or errors when scripts fail. 6 | -------------------------------------------------------------------------------- /send/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "log" 9 | "os" 10 | 11 | "github.com/keybase/slackbot" 12 | "github.com/nlopes/slack" 13 | ) 14 | 15 | var ignoreError = flag.Bool("i", false, "Ignore error (always exit 0)") 16 | 17 | func handleError(s string, text string) { 18 | if *ignoreError { 19 | log.Printf("[Unable to send: %s] %s", s, text) 20 | os.Exit(0) 21 | } 22 | log.Fatal(s) 23 | } 24 | 25 | func main() { 26 | flag.Parse() 27 | text := flag.Arg(0) 28 | 29 | channel := os.Getenv("SLACK_CHANNEL") 30 | if channel == "" { 31 | handleError("SLACK_CHANNEL is not set", text) 32 | } 33 | 34 | api := slack.New(slackbot.GetTokenFromEnv()) 35 | // api.SetDebug(true) 36 | 37 | channelIDs, err := slackbot.LoadChannelIDs(*api) 38 | if err != nil { 39 | handleError(err.Error(), text) 40 | } 41 | 42 | params := slack.NewPostMessageParameters() 43 | params.AsUser = true 44 | channelID := channelIDs[channel] 45 | _, _, err = api.PostMessage(channelID, text, params) 46 | if err != nil { 47 | handleError(err.Error(), text) 48 | } else { 49 | log.Printf("[%s (%s)] %s\n", channel, channelID, text) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | package slackbot 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/nlopes/slack" 7 | ) 8 | 9 | // SlackBotBackend is a Slack bot backend 10 | type SlackBotBackend struct { //nolint 11 | api *slack.Client 12 | rtm *slack.RTM 13 | 14 | channelIDs map[string]string 15 | } 16 | 17 | // NewSlackBotBackend constructs a bot backend from a Slack token 18 | func NewSlackBotBackend(token string) (BotBackend, error) { 19 | api := slack.New(token) 20 | // api.SetDebug(true) 21 | 22 | channelIDs, err := LoadChannelIDs(*api) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | bot := &SlackBotBackend{} 28 | bot.api = api 29 | bot.rtm = api.NewRTM() 30 | bot.channelIDs = channelIDs 31 | return bot, nil 32 | } 33 | 34 | // SendMessage sends a message to a channel 35 | func (b *SlackBotBackend) SendMessage(text string, channel string) { 36 | cid := b.channelIDs[channel] 37 | if cid == "" { 38 | cid = channel 39 | } 40 | 41 | if channel == "" { 42 | log.Printf("No channel to send message: %s", text) 43 | return 44 | } 45 | 46 | if b.rtm != nil { 47 | b.rtm.SendMessage(b.rtm.NewOutgoingMessage(text, cid)) 48 | } else { 49 | log.Printf("Unable to send message: %s", text) 50 | } 51 | } 52 | 53 | // Listen starts listening on the connection 54 | func (b *SlackBotBackend) Listen(runner BotCommandRunner) { 55 | go b.rtm.ManageConnection() 56 | 57 | auth, err := b.api.AuthTest() 58 | if err != nil { 59 | panic(err) 60 | } 61 | // The Slack bot "tuxbot" should expect commands to start with "!tuxbot". 62 | log.Printf("Connected to Slack as %q", auth.User) 63 | commandPrefix := "!" + auth.User 64 | 65 | Loop: 66 | for { 67 | msg := <-b.rtm.IncomingEvents 68 | switch ev := msg.Data.(type) { 69 | case *slack.HelloEvent: 70 | 71 | case *slack.ConnectedEvent: 72 | 73 | case *slack.MessageEvent: 74 | args := parseInput(ev.Text) 75 | if len(args) > 0 && args[0] == commandPrefix { 76 | cmd := args[1:] 77 | if err := runner.RunCommand(cmd, ev.Channel); err != nil { 78 | log.Printf("failed to run command: %s\n", err) 79 | } 80 | } 81 | 82 | case *slack.PresenceChangeEvent: 83 | // log.Printf("Presence Change: %v\n", ev) 84 | 85 | case *slack.LatencyReport: 86 | // log.Printf("Current latency: %v\n", ev.Value) 87 | 88 | case *slack.RTMError: 89 | log.Printf("Error: %s\n", ev.Error()) 90 | 91 | case *slack.InvalidAuthEvent: 92 | log.Printf("Invalid credentials\n") 93 | break Loop 94 | 95 | default: 96 | // log.Printf("Unexpected: %v\n", msg.Data) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /systemd/README.md: -------------------------------------------------------------------------------- 1 | If you want to run a single build by hand, see the README in 2 | https://github.com/keybase/client: packaging/linux/README.md 3 | 4 | ### Instructions for getting the Keybase buildbot running on Linux 5 | 6 | - Create an account called "keybasebuild": `sudo useradd -m keybasebuild` 7 | - NOTE: If you use a different name, you will need to tweak the 8 | *.service files in this directory. They hardcode paths that include 9 | the username. 10 | - Add user "keybasebuild" to the "docker" group: `sudo gpasswd -a keybasebuild docker` 11 | - Allow the keybasebuild account to start systemd services on boot: `sudo loginctl enable-linger keybasebuild` 12 | - Do a *real log in* as that user. That means either a graphical 13 | desktop, or via SSH. In particular, if you try to `sudo` into this 14 | user, several steps below will fail. 15 | - Install all the credentials you need. We have a sepate "build-linux" 16 | repo for this with its own README-- ask Max where it is. 17 | - Clone three repos into /home/keybasebuild: 18 | - https://github.com/keybase/client 19 | - https://github.com/keybase/kbfs 20 | - https://github.com/keybase/slackbot (this repo) 21 | - Enable the systemd service files. (These are the commands that will 22 | fail if you don't have a real login.) 23 | - `mkdir -p ~/.config/systemd/user` 24 | - `cp ~/slackbot/systemd/keybase.*.{service,timer} ~/.config/systemd/user/` 25 | - `systemctl --user enable --now keybase.keybot.service` 26 | - `systemctl --user enable --now keybase.buildplease.timer` 27 | - Take the bot out of dry-run mode by messaging `!tuxbot toggle-dryrun` 28 | in the #bot channel. 29 | 30 | For stathat logging, add a `STATHAT_EZKEY` env variable to the envfile used by the unit. 31 | -------------------------------------------------------------------------------- /systemd/keybase.buildplease.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=send "!tuxbot build linux" to the Slack #bot channel 3 | 4 | [Service] 5 | EnvironmentFile=/home/keybasebuild/keybot.env 6 | ExecStart=/home/keybasebuild/slackbot/systemd/send.sh '!tuxbot build linux --skip-ci --nightly' 7 | -------------------------------------------------------------------------------- /systemd/keybase.buildplease.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=run keybase.buildplease.service every weekday at noon 3 | 4 | [Timer] 5 | OnCalendar=Mon-Fri 12:00 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /systemd/keybase.keybot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=The Keybase Slack robot responsible for the Linux build 3 | 4 | [Service] 5 | EnvironmentFile=/home/keybasebuild/keybot.env 6 | ExecStart=/home/keybasebuild/slackbot/systemd/run_keybot.sh 7 | 8 | [Install] 9 | WantedBy=default.target 10 | -------------------------------------------------------------------------------- /systemd/nightly-stathat-success.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -X POST -d "stat=tuxbot - nightly - success&ezkey=$STATHAT_EZKEY&count=1" https://api.stathat.com/ez 3 | -------------------------------------------------------------------------------- /systemd/prerelease.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e -u -o pipefail 4 | 5 | cd ~/client 6 | 7 | git checkout -f master 8 | 9 | git pull --ff-only 10 | 11 | ./packaging/linux/docker_build.sh prerelease HEAD 12 | -------------------------------------------------------------------------------- /systemd/run_keybot.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e -u -o pipefail 4 | 5 | cd "$(dirname "$BASH_SOURCE")/../tuxbot" 6 | 7 | export GOPATH="$(pwd)/gopath" 8 | 9 | if ! [ -e "$GOPATH" ] ; then 10 | # Build the local GOPATH. 11 | mkdir -p "$GOPATH/src/github.com/keybase" 12 | ln -s "$(git rev-parse --show-toplevel)" gopath/src/github.com/keybase/slackbot 13 | fi 14 | 15 | go install github.com/keybase/slackbot/tuxbot 16 | 17 | # Wait for the network. 18 | while ! ping -c 3 slack.com ; do 19 | sleep 1 20 | done 21 | 22 | exec "$GOPATH/bin/tuxbot" 23 | -------------------------------------------------------------------------------- /systemd/send.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -e -u -o pipefail 4 | 5 | cd "$(dirname "$BASH_SOURCE")/../send" 6 | 7 | export GOPATH="$(pwd)/gopath" 8 | 9 | if ! [ -e "$GOPATH" ] ; then 10 | # Build the local GOPATH. 11 | mkdir -p "$GOPATH/src/github.com/keybase" 12 | ln -s "$(git rev-parse --show-toplevel)" gopath/src/github.com/keybase/slackbot 13 | fi 14 | 15 | go get -v github.com/keybase/slackbot/send 16 | go install github.com/keybase/slackbot/send 17 | 18 | exec "$GOPATH/bin/send" "$@" 19 | -------------------------------------------------------------------------------- /systemd/tuxbot.nightly.stathat-noop.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=send stathat success unconditionally 3 | 4 | [Service] 5 | EnvironmentFile=/home/keybasebuild/keybot.env 6 | ExecStart=/home/keybasebuild/slackbot/systemd/nightly-stathat-success.sh 7 | -------------------------------------------------------------------------------- /systemd/tuxbot.nightly.stathat-noop.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=send stathat success on weekends when nightly doesn't run 3 | 4 | [Timer] 5 | OnCalendar=Sat,Sun 12:00 6 | Persistent=true 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /tuxbot/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | 9 | "github.com/keybase/slackbot" 10 | ) 11 | 12 | func main() { 13 | backend, err := slackbot.NewSlackBotBackend(slackbot.GetTokenFromEnv()) 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | bot := slackbot.NewBot(slackbot.ReadConfigOrDefault(), "tuxbot", "", backend) 18 | 19 | bot.AddCommand("date", slackbot.NewExecCommand("/bin/date", nil, true, "Show the current date", bot.Config())) 20 | bot.AddCommand("pause", slackbot.NewPauseCommand(bot.Config())) 21 | bot.AddCommand("resume", slackbot.NewResumeCommand(bot.Config())) 22 | bot.AddCommand("config", slackbot.NewShowConfigCommand(bot.Config())) 23 | bot.AddCommand("toggle-dryrun", slackbot.NewToggleDryRunCommand(bot.Config())) 24 | 25 | // Extension 26 | ext := &tuxbot{bot: bot} 27 | runFn := func(channel string, args []string) (string, error) { 28 | return ext.Run(bot, channel, args) 29 | } 30 | bot.SetDefault(slackbot.NewFuncCommand(runFn, "Extension", bot.Config())) 31 | bot.SetHelp(bot.HelpMessage() + "\n\n" + ext.Help(bot)) 32 | 33 | log.Println("Started tuxbot") 34 | bot.Listen() 35 | } 36 | -------------------------------------------------------------------------------- /tuxbot/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/keybase/slackbot" 11 | ) 12 | 13 | func TestBuildLinux(t *testing.T) { 14 | bot, err := slackbot.NewTestBot() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | ext := &tuxbot{} 19 | out, err := ext.Run(bot, "", []string{"build", "linux"}) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | if out != "Dry Run: Doing that would run `prerelease.sh`" { 24 | t.Errorf("Unexpected output: %s", out) 25 | } 26 | } 27 | 28 | func TestInvalidUsage(t *testing.T) { 29 | bot, err := slackbot.NewTestBot() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | ext := &tuxbot{} 34 | out, err := ext.Run(bot, "", []string{"build", "oops"}) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if !strings.HasPrefix(out, "```\nI don't know what you mean by") { 39 | t.Errorf("Unexpected output: %s", out) 40 | } 41 | } 42 | 43 | func TestBuildLinuxSkipCI(t *testing.T) { 44 | bot, err := slackbot.NewTestBot() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | ext := &tuxbot{} 49 | out, err := ext.Run(bot, "", []string{"build", "linux", "--skip-ci"}) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if out != "Dry Run: Doing that would run `prerelease.sh` with NOWAIT=1 set" { 54 | t.Errorf("Unexpected output: %s", out) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tuxbot/tuxbot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "os/exec" 13 | "os/user" 14 | "path/filepath" 15 | 16 | "github.com/keybase/slackbot" 17 | "github.com/keybase/slackbot/cli" 18 | "github.com/nlopes/slack" 19 | kingpin "gopkg.in/alecthomas/kingpin.v2" 20 | ) 21 | 22 | func (t *tuxbot) linuxBuildFunc(channel string, _ []string, skipCI bool, nightly bool) (string, error) { 23 | currentUser, err := user.Current() 24 | if err != nil { 25 | return "", err 26 | } 27 | t.bot.SendMessage("building linux!!!", channel) 28 | prereleaseScriptPath := filepath.Join(currentUser.HomeDir, "slackbot/systemd/prerelease.sh") 29 | prereleaseCmd := exec.Command(prereleaseScriptPath) 30 | prereleaseCmd.Stdout = os.Stdout 31 | prereleaseCmd.Stderr = os.Stderr 32 | prereleaseCmd.Env = os.Environ() 33 | if skipCI { 34 | prereleaseCmd.Env = append(prereleaseCmd.Env, "NOWAIT=1") 35 | t.bot.SendMessage("--- with NOWAIT=1", channel) 36 | } 37 | if nightly { 38 | prereleaseCmd.Env = append(prereleaseCmd.Env, "KEYBASE_NIGHTLY=1") 39 | t.bot.SendMessage("--- with KEYBASE_NIGHTLY=1", channel) 40 | } 41 | err = prereleaseCmd.Run() 42 | if err != nil { 43 | journal, _ := exec.Command("journalctl", "--since=today", "--user-unit", "keybase.keybot.service").CombinedOutput() 44 | api := slack.New(slackbot.GetTokenFromEnv()) 45 | snippetFile := slack.FileUploadParameters{ 46 | Channels: []string{channel}, 47 | Title: "failed build output", 48 | Content: string(journal), 49 | } 50 | _, _ = api.UploadFile(snippetFile) // ignore errors here for now 51 | return "FAILURE", err 52 | } 53 | return "SUCCESS", nil 54 | } 55 | 56 | type tuxbot struct { 57 | bot *slackbot.Bot 58 | } 59 | 60 | func (t *tuxbot) Run(bot *slackbot.Bot, channel string, args []string) (string, error) { 61 | app := kingpin.New("tuxbot", "Command parser for tuxbot") 62 | app.Terminate(nil) 63 | stringBuffer := new(bytes.Buffer) 64 | app.Writer(stringBuffer) 65 | 66 | build := app.Command("build", "Build things") 67 | buildLinux := build.Command("linux", "Start a linux build") 68 | buildLinuxSkipCI := buildLinux.Flag("skip-ci", "Whether to skip CI").Bool() 69 | buildLinuxNightly := buildLinux.Flag("nightly", "Trigger a nightly build instead of main channel").Bool() 70 | 71 | cmd, usage, err := cli.Parse(app, args, stringBuffer) 72 | if usage != "" || err != nil { 73 | return usage, err 74 | } 75 | 76 | if cmd == buildLinux.FullCommand() { 77 | if bot.Config().DryRun() { 78 | if *buildLinuxSkipCI { 79 | return "Dry Run: Doing that would run `prerelease.sh` with NOWAIT=1 set", nil 80 | } 81 | return "Dry Run: Doing that would run `prerelease.sh`", nil 82 | } 83 | if bot.Config().Paused() { 84 | return "I'm paused so I can't do that, but I would have run `prerelease.sh`", nil 85 | } 86 | 87 | ret, err := t.linuxBuildFunc(channel, args, *buildLinuxSkipCI, *buildLinuxNightly) 88 | 89 | var stathatErr error 90 | if err == nil { 91 | stathatErr = postStathat("tuxbot - nightly - success", "1") 92 | } else { 93 | stathatErr = postStathat("tuxbot - nightly - failure", "1") 94 | } 95 | if stathatErr != nil { 96 | return fmt.Sprintf("stathat error. original message: %s", ret), 97 | fmt.Errorf("stathat error: %s. original error: %s", stathatErr, err) 98 | } 99 | 100 | return ret, err 101 | } 102 | 103 | return cmd, nil 104 | } 105 | 106 | func postStathat(key string, count string) error { 107 | ezkey := os.Getenv("STATHAT_EZKEY") 108 | if ezkey == "" { 109 | return fmt.Errorf("no stathat key") 110 | } 111 | vals := url.Values{ 112 | "ezkey": {ezkey}, 113 | "stat": {key}, 114 | "count": {count}, 115 | } 116 | _, err := http.PostForm("https://api.stathat.com/ez", vals) 117 | return err 118 | } 119 | 120 | func (t *tuxbot) Help(bot *slackbot.Bot) string { 121 | out, err := t.Run(bot, "", nil) 122 | if err != nil { 123 | return fmt.Sprintf("Error getting help: %s", err) 124 | } 125 | return out 126 | } 127 | --------------------------------------------------------------------------------