├── .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 | [](https://github.com/keybase/slackbot/actions)
4 | [](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 |
--------------------------------------------------------------------------------