├── .gitignore
├── systemd
├── nightly-stathat-success.sh
├── prerelease.sh
├── keybase.buildplease.timer
├── tuxbot.nightly.stathat-noop.timer
├── tuxbot.nightly.stathat-noop.service
├── keybase.buildplease.service
├── keybase.keybot.service
├── send.sh
├── run_keybot.sh
└── README.md
├── send
├── README.md
└── main.go
├── keybot
├── keybot.sh
├── README.md
├── keybase.keybot.plist
├── main_test.go
├── main.go
├── keybot.go
└── winbot.go
├── .pre-commit-config.yaml
├── scripts
├── run_and_send_stdout.sh
├── node_module_clean.sh
├── dumplog.sh
├── release.broken.sh
├── send.sh
├── smoketest.sh
├── upgrade.sh
├── git_clean.sh
├── run.sh
├── git_diff.sh
├── release.promote.sh
└── check_status_and_pull.sh
├── README.md
├── launchd
├── plist_test.go
├── command.go
├── example
│ └── schedule-command.plist
└── plist.go
├── api.go
├── bot_test.go
├── tuxbot
├── main.go
├── main_test.go
└── tuxbot.go
├── hybrid.go
├── .github
└── workflows
│ └── ci.yml
├── examplebot
├── extension.go
└── main.go
├── cli
└── cli.go
├── go.mod
├── .golangci.yml
├── kbchat.go
├── command.go
├── slack.go
├── go.sum
├── config.go
└── bot.go
/.gitignore:
--------------------------------------------------------------------------------
1 | gopath
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | type HybridBackendMember struct {
24 | Backend BotBackend
25 | Channel string
26 | }
27 |
28 | type HybridBackend struct {
29 | backends []HybridBackendMember
30 | }
31 |
32 | func NewHybridBackend(backends ...HybridBackendMember) *HybridBackend {
33 | return &HybridBackend{
34 | backends: backends,
35 | }
36 | }
37 |
38 | func (b *HybridBackend) SendMessage(text string, _ string) {
39 | for _, backend := range b.backends {
40 | backend.Backend.SendMessage(text, backend.Channel)
41 | }
42 | }
43 |
44 | func (b *HybridBackend) Listen(runner BotCommandRunner) {
45 | var wg sync.WaitGroup
46 | for _, backend := range b.backends {
47 | wg.Add(1)
48 | go func(b HybridBackendMember) {
49 | b.Backend.Listen(newHybridRunner(runner, b.Channel))
50 | wg.Done()
51 | }(backend)
52 | }
53 | wg.Wait()
54 | }
55 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | schedule:
11 | # Run daily at 2 AM UTC to check for new vulnerabilities
12 | - cron: "0 2 * * *"
13 |
14 | permissions:
15 | contents: read
16 |
17 | concurrency:
18 | group: ${{ github.workflow }}-${{ github.ref }}
19 | cancel-in-progress: true
20 |
21 | jobs:
22 | test:
23 | timeout-minutes: 15
24 | strategy:
25 | matrix:
26 | go-version: [1.25.x, 1.24.x]
27 | os: [ubuntu-latest, macos-latest]
28 | runs-on: ${{ matrix.os }}
29 | steps:
30 | - uses: actions/checkout@v6
31 | with:
32 | persist-credentials: false
33 |
34 | - uses: actions/setup-go@v6
35 | with:
36 | go-version: ${{ matrix.go-version }}
37 | cache: true
38 |
39 | - name: golangci-lint
40 | uses: golangci/golangci-lint-action@v9
41 | with:
42 | version: v2.7.2
43 |
44 | - name: Build
45 | run: go build -v ./...
46 |
47 | - name: Run govulncheck
48 | uses: golang/govulncheck-action@v1
49 | with:
50 | go-version-input: ${{ matrix.go-version }}
51 |
52 | - name: Test
53 | run: go test -race ./...
54 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | if _, writeErr := 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())); writeErr != nil {
40 | log.Printf("Error writing error message: %s", writeErr)
41 | }
42 | // Print out help page if there was an error parsing command
43 | app.Usage([]string{})
44 | }
45 |
46 | if stringBuffer.Len() > 0 {
47 | return "", slackbot.BlockQuote(stringBuffer.String()), nil
48 | }
49 |
50 | return cmd, "", err
51 | }
52 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/keybase/slackbot
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.25.5
6 |
7 | require (
8 | github.com/keybase/go-keybase-chat-bot v0.0.0-20251212163122-450fd0812017
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.11.1 // 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 |
3 | run:
4 | timeout: 5m
5 | tests: true
6 |
7 | formatters:
8 | enable:
9 | - gofumpt
10 |
11 | linters:
12 | enable:
13 | # Core recommended linters
14 | - errcheck # Checks for unchecked errors
15 | - govet # Go vet checks
16 | - ineffassign # Detects ineffectual assignments
17 | - staticcheck # Advanced static analysis
18 | - unused # Finds unused code
19 |
20 | # Code quality
21 | - misspell # Finds commonly misspelled words
22 | - unconvert # Unnecessary type conversions (already enabled in original)
23 | - unparam # Finds unused function parameters
24 | - gocritic # Various checks (already enabled in original)
25 | - revive # Fast, configurable linter (already enabled in original)
26 |
27 | # Security and best practices
28 | - gosec # Security-focused linter
29 | - bodyclose # Checks HTTP response body closed
30 | - noctx # Finds HTTP requests without context
31 |
32 | settings:
33 | gocritic:
34 | disabled-checks:
35 | - ifElseChain
36 | - elseif
37 |
38 | govet:
39 | enable-all: true
40 | disable:
41 | - shadow
42 | - fieldalignment
43 |
44 | revive:
45 | enable-all-rules: false
46 |
47 | exclusions:
48 | rules:
49 | # Exclude specific revive rules
50 | - linters:
51 | - revive
52 | text: "package-comments"
53 |
54 | - linters:
55 | - revive
56 | text: "exported"
57 |
58 | # Exclude specific staticcheck rules
59 | - linters:
60 | - staticcheck
61 | text: "ST1005"
62 |
63 | # Exclude specific gocritic rules
64 | - linters:
65 | - gocritic
66 | text: "ifElseChain"
67 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | //nolint:gosec,noctx // launchctl is a trusted system binary with safe arguments, no context available
28 | if _, err := exec.Command("/bin/launchctl", "unload", c.plistPath).CombinedOutput(); err != nil {
29 | return "", fmt.Errorf("Error in launchctl unload: %s", err)
30 | }
31 |
32 | //nolint:gosec,noctx // launchctl is a trusted system binary with safe arguments, no context available
33 | if _, err := exec.Command("/bin/launchctl", "load", c.plistPath).CombinedOutput(); err != nil {
34 | return "", fmt.Errorf("Error in launchctl load: %s", err)
35 | }
36 |
37 | //nolint:gosec,noctx // launchctl is a trusted system binary with safe arguments, no context available
38 | if _, err := exec.Command("/bin/launchctl", "start", c.label).CombinedOutput(); err != nil {
39 | return "", fmt.Errorf("Error in launchctl start: %s", err)
40 | }
41 |
42 | return "", nil
43 | }
44 |
45 | // Stop a launchd job
46 | func Stop(label string) (string, error) {
47 | //nolint:noctx // launchctl is a trusted system binary, no context available
48 | if _, err := exec.Command("/bin/launchctl", "stop", label).CombinedOutput(); err != nil {
49 | return "", fmt.Errorf("Error in launchctl stop: %s", err)
50 | }
51 | return fmt.Sprintf("I stopped the job `%s`.", label), nil
52 | }
53 |
54 | // ShowResult decides whether to show the results from the exec
55 | func (c StartCommand) ShowResult() bool {
56 | return false
57 | }
58 |
59 | // Description describes the command
60 | func (c StartCommand) Description() string {
61 | return fmt.Sprintf("Run launchd job (%s)", c.label)
62 | }
63 |
64 | // Label returns job label
65 | func (c StartCommand) Label() string {
66 | return c.label
67 | }
68 |
--------------------------------------------------------------------------------
/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 | //nolint:gosec,noctx // Command execution is the purpose of this bot, no context available
45 | out, err := exec.Command(c.exec, c.args...).CombinedOutput()
46 | outAsString := string(out)
47 | return outAsString, err
48 | }
49 |
50 | // ShowResult decides whether to show the results from the exec
51 | func (c execCommand) ShowResult() bool {
52 | return c.config.DryRun() || c.showResult
53 | }
54 |
55 | // Description describes the command
56 | func (c execCommand) Description() string {
57 | return c.description
58 | }
59 |
60 | // CommandFn is the function that is run for this command
61 | type CommandFn func(channel string, args []string) (string, error)
62 |
63 | // NewFuncCommand creates a new function command
64 | func NewFuncCommand(fn CommandFn, desc string, config Config) Command {
65 | return funcCommand{
66 | fn: fn,
67 | desc: desc,
68 | config: config,
69 | }
70 | }
71 |
72 | type funcCommand struct {
73 | desc string
74 | fn CommandFn
75 | config Config
76 | }
77 |
78 | func (c funcCommand) Run(channel string, args []string) (string, error) {
79 | return c.fn(channel, args)
80 | }
81 |
82 | func (c funcCommand) ShowResult() bool {
83 | return true
84 | }
85 |
86 | func (c funcCommand) Description() string {
87 | return c.desc
88 | }
89 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-20251212163122-450fd0812017 h1:lB6jgDag58Ie9yfLSGDQiUZt60zPyRpK6aWCtovQeSo=
10 | github.com/keybase/go-keybase-chat-bot v0.0.0-20251212163122-450fd0812017/go.mod h1:wl5lBoVNkepL8Hzs7jyqg3GS6U+by4yQeNr7oT0Evt0=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
24 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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 |
--------------------------------------------------------------------------------
/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 | "log"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "os/exec"
14 | "os/user"
15 | "path/filepath"
16 |
17 | "github.com/keybase/slackbot"
18 | "github.com/keybase/slackbot/cli"
19 | "github.com/nlopes/slack"
20 | kingpin "gopkg.in/alecthomas/kingpin.v2"
21 | )
22 |
23 | func (t *tuxbot) linuxBuildFunc(channel string, _ []string, skipCI bool, nightly bool) (string, error) {
24 | currentUser, err := user.Current()
25 | if err != nil {
26 | return "", err
27 | }
28 | t.bot.SendMessage("building linux!!!", channel)
29 | prereleaseScriptPath := filepath.Join(currentUser.HomeDir, "slackbot/systemd/prerelease.sh")
30 | //nolint:gosec,noctx // Executing build script from known location in user's home directory, no context available
31 | prereleaseCmd := exec.Command(prereleaseScriptPath)
32 | prereleaseCmd.Stdout = os.Stdout
33 | prereleaseCmd.Stderr = os.Stderr
34 | prereleaseCmd.Env = os.Environ()
35 | if skipCI {
36 | prereleaseCmd.Env = append(prereleaseCmd.Env, "NOWAIT=1")
37 | t.bot.SendMessage("--- with NOWAIT=1", channel)
38 | }
39 | if nightly {
40 | prereleaseCmd.Env = append(prereleaseCmd.Env, "KEYBASE_NIGHTLY=1")
41 | t.bot.SendMessage("--- with KEYBASE_NIGHTLY=1", channel)
42 | }
43 | err = prereleaseCmd.Run()
44 | if err != nil {
45 | //nolint:noctx // Error logging command, no request context available
46 | journal, journalErr := exec.Command("journalctl", "--since=today", "--user-unit", "keybase.keybot.service").CombinedOutput()
47 | if journalErr != nil {
48 | log.Printf("Error getting journal: %s", journalErr)
49 | }
50 | api := slack.New(slackbot.GetTokenFromEnv())
51 | snippetFile := slack.FileUploadParameters{
52 | Channels: []string{channel},
53 | Title: "failed build output",
54 | Content: string(journal),
55 | }
56 | if _, uploadErr := api.UploadFile(snippetFile); uploadErr != nil {
57 | log.Printf("Error uploading build output: %s", uploadErr)
58 | }
59 | return "FAILURE", err
60 | }
61 | return "SUCCESS", nil
62 | }
63 |
64 | type tuxbot struct {
65 | bot *slackbot.Bot
66 | }
67 |
68 | func (t *tuxbot) Run(bot *slackbot.Bot, channel string, args []string) (string, error) {
69 | app := kingpin.New("tuxbot", "Command parser for tuxbot")
70 | app.Terminate(nil)
71 | stringBuffer := new(bytes.Buffer)
72 | app.Writer(stringBuffer)
73 |
74 | build := app.Command("build", "Build things")
75 | buildLinux := build.Command("linux", "Start a linux build")
76 | buildLinuxSkipCI := buildLinux.Flag("skip-ci", "Whether to skip CI").Bool()
77 | buildLinuxNightly := buildLinux.Flag("nightly", "Trigger a nightly build instead of main channel").Bool()
78 |
79 | cmd, usage, err := cli.Parse(app, args, stringBuffer)
80 | if usage != "" || err != nil {
81 | return usage, err
82 | }
83 |
84 | if cmd == buildLinux.FullCommand() {
85 | if bot.Config().DryRun() {
86 | if *buildLinuxSkipCI {
87 | return "Dry Run: Doing that would run `prerelease.sh` with NOWAIT=1 set", nil
88 | }
89 | return "Dry Run: Doing that would run `prerelease.sh`", nil
90 | }
91 | if bot.Config().Paused() {
92 | return "I'm paused so I can't do that, but I would have run `prerelease.sh`", nil
93 | }
94 |
95 | ret, err := t.linuxBuildFunc(channel, args, *buildLinuxSkipCI, *buildLinuxNightly)
96 |
97 | var stathatErr error
98 | if err == nil {
99 | stathatErr = postStathat("tuxbot - nightly - success", "1")
100 | } else {
101 | stathatErr = postStathat("tuxbot - nightly - failure", "1")
102 | }
103 | if stathatErr != nil {
104 | return fmt.Sprintf("stathat error. original message: %s", ret),
105 | fmt.Errorf("stathat error: %s. original error: %s", stathatErr, err)
106 | }
107 |
108 | return ret, err
109 | }
110 |
111 | return cmd, nil
112 | }
113 |
114 | func postStathat(key string, count string) error {
115 | ezkey := os.Getenv("STATHAT_EZKEY")
116 | if ezkey == "" {
117 | return fmt.Errorf("no stathat key")
118 | }
119 | vals := url.Values{
120 | "ezkey": {ezkey},
121 | "stat": {key},
122 | "count": {count},
123 | }
124 | //nolint:noctx // Simple fire-and-forget stat reporting, no request context available
125 | resp, err := http.PostForm("https://api.stathat.com/ez", vals)
126 | if resp != nil {
127 | defer func() {
128 | if closeErr := resp.Body.Close(); closeErr != nil {
129 | log.Printf("Error closing response body: %s", closeErr)
130 | }
131 | }()
132 | }
133 | return err
134 | }
135 |
136 | func (t *tuxbot) Help(bot *slackbot.Bot) string {
137 | out, err := t.Run(bot, "", nil)
138 | if err != nil {
139 | return fmt.Sprintf("Error getting help: %s", err)
140 | }
141 | return out
142 | }
143 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | if err != nil {
58 | return "", err
59 | }
60 |
61 | return filepath.Join(currentUser.HomeDir, ".keybot"), nil
62 | }
63 |
64 | // NewConfig returns default config
65 | func NewConfig(dryRun, paused bool) Config {
66 | return &config{
67 | DryRunField: dryRun,
68 | PausedField: paused,
69 | }
70 | }
71 |
72 | // ReadConfigOrDefault returns config stored or default
73 | func ReadConfigOrDefault() Config {
74 | cfg := readConfigOrDefault()
75 | return &cfg
76 | }
77 |
78 | func readConfigOrDefault() config {
79 | defaultConfig := config{
80 | DryRunField: true,
81 | PausedField: false,
82 | }
83 |
84 | path, err := getConfigPath()
85 | if err != nil {
86 | return defaultConfig
87 | }
88 |
89 | fileBytes, err := os.ReadFile(filepath.Clean(path))
90 | if err != nil {
91 | return defaultConfig
92 | }
93 |
94 | var cfg config
95 | err = json.Unmarshal(fileBytes, &cfg)
96 | if err != nil {
97 | log.Printf("Couldn't read config file: %s\n", err)
98 | return defaultConfig
99 | }
100 |
101 | return cfg
102 | }
103 |
104 | func (c config) Save() error {
105 | b, err := json.Marshal(c)
106 | if err != nil {
107 | return err
108 | }
109 |
110 | path, err := getConfigPath()
111 | if err != nil {
112 | return err
113 | }
114 |
115 | err = os.WriteFile(filepath.Clean(path), b, 0o644) //nolint:gosec // Config file should be user-readable
116 | if err != nil {
117 | return err
118 | }
119 |
120 | return nil
121 | }
122 |
123 | // NewShowConfigCommand returns command that shows config
124 | func NewShowConfigCommand(config Config) Command {
125 | return &showConfigCommand{config: config}
126 | }
127 |
128 | type showConfigCommand struct {
129 | config Config
130 | }
131 |
132 | func (c showConfigCommand) Run(_ string, _ []string) (string, error) {
133 | if !c.config.Paused() && !c.config.DryRun() {
134 | return "I'm running normally.", nil
135 | }
136 | lines := []string{}
137 | if c.config.Paused() {
138 | lines = append(lines, "I'm paused.")
139 | }
140 | if c.config.DryRun() {
141 | lines = append(lines, "I'm in dry run mode.")
142 | }
143 | return strings.Join(lines, " "), nil
144 | }
145 |
146 | func (c showConfigCommand) ShowResult() bool {
147 | return true
148 | }
149 |
150 | func (c showConfigCommand) Description() string {
151 | return "Shows config"
152 | }
153 |
154 | // NewToggleDryRunCommand returns toggle dry run command
155 | func NewToggleDryRunCommand(config Config) Command {
156 | return &toggleDryRunCommand{config: config}
157 | }
158 |
159 | type toggleDryRunCommand struct {
160 | config Config
161 | }
162 |
163 | func (c *toggleDryRunCommand) Run(_ string, _ []string) (string, error) {
164 | c.config.SetDryRun(!c.config.DryRun())
165 | err := c.config.Save()
166 | if err != nil {
167 | return "", err
168 | }
169 |
170 | if c.config.DryRun() {
171 | return "We are in dry run mode.", nil
172 | }
173 | return "We are not longer in dry run mode", nil
174 | }
175 |
176 | func (c toggleDryRunCommand) ShowResult() bool {
177 | return true
178 | }
179 |
180 | func (c toggleDryRunCommand) Description() string {
181 | return "Toggles the dry run mode"
182 | }
183 |
184 | // NewPauseCommand pauses
185 | func NewPauseCommand(config Config) Command {
186 | return &pauseCommand{
187 | config: config,
188 | pauses: true,
189 | }
190 | }
191 |
192 | // NewResumeCommand resumes
193 | func NewResumeCommand(config Config) Command {
194 | return &pauseCommand{
195 | config: config,
196 | pauses: false,
197 | }
198 | }
199 |
200 | type pauseCommand struct {
201 | config Config
202 | pauses bool
203 | }
204 |
205 | // Run toggles the dry run state. (Itself is never run under dry run mode)
206 | func (c *pauseCommand) Run(_ string, _ []string) (string, error) {
207 | log.Printf("Setting paused: %v\n", c.pauses)
208 | c.config.SetPaused(c.pauses)
209 | err := c.config.Save()
210 | if err != nil {
211 | return "", err
212 | }
213 |
214 | if c.config.Paused() {
215 | return "I am paused.", nil
216 | }
217 | return "I have resumed.", nil
218 | }
219 |
220 | // ShowResult always shows results for toggling dry run
221 | func (c pauseCommand) ShowResult() bool {
222 | return true
223 | }
224 |
225 | // Description describes what it does
226 | func (c pauseCommand) Description() string {
227 | if c.pauses {
228 | return "Pauses the bot"
229 | }
230 | return "Resumes the bot"
231 | }
232 |
--------------------------------------------------------------------------------
/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 | if _, err := fmt.Fprintln(w, "Command\tDescription"); err != nil {
73 | log.Printf("Error writing help header: %s", err)
74 | return "Error generating help message"
75 | }
76 | for _, trigger := range b.triggers() {
77 | command := b.commands[trigger]
78 | if _, err := fmt.Fprintf(w, "%s\t%s\n", trigger, command.Description()); err != nil {
79 | log.Printf("Error writing help command: %s", err)
80 | return "Error generating help message"
81 | }
82 | }
83 | if err := w.Flush(); err != nil {
84 | log.Printf("Error flushing help writer: %s", err)
85 | return "Error generating help message"
86 | }
87 | return BlockQuote(buf.String())
88 | }
89 |
90 | func (b *Bot) SetHelp(help string) {
91 | b.help = help
92 | }
93 |
94 | func (b *Bot) Label() string {
95 | return b.label
96 | }
97 |
98 | func (b *Bot) SetDefault(command Command) {
99 | b.defaultCommand = command
100 | }
101 |
102 | // RunCommand runs a command
103 | func (b *Bot) RunCommand(args []string, channel string) error {
104 | if len(args) == 0 || args[0] == "help" {
105 | b.sendHelpMessage(channel)
106 | return nil
107 | }
108 |
109 | command, ok := b.commands[args[0]]
110 | if !ok {
111 | if b.defaultCommand != nil {
112 | command = b.defaultCommand
113 | } else {
114 | return fmt.Errorf("Unrecognized command: %q", args)
115 | }
116 | }
117 |
118 | if args[0] != "resume" && args[0] != "config" && b.Config().Paused() {
119 | b.backend.SendMessage("I can't do that, I'm paused.", channel)
120 | return nil
121 | }
122 |
123 | go b.run(args, command, channel)
124 | return nil
125 | }
126 |
127 | func (b *Bot) run(args []string, command Command, channel string) {
128 | out, err := command.Run(channel, args)
129 | if err != nil {
130 | log.Printf("Error %s running: %#v; %s\n", err, command, out)
131 | b.backend.SendMessage(fmt.Sprintf("Oops, there was an error in %q:\n%s", strings.Join(args, " "),
132 | BlockQuote(out)), channel)
133 | return
134 | }
135 | log.Printf("Output: %s\n", out)
136 | if command.ShowResult() {
137 | b.backend.SendMessage(out, channel)
138 | }
139 | }
140 |
141 | func (b *Bot) sendHelpMessage(channel string) {
142 | help := b.help
143 | if help == "" {
144 | help = b.HelpMessage()
145 | }
146 | b.backend.SendMessage(help, channel)
147 | }
148 |
149 | func (b *Bot) SendMessage(text string, channel string) {
150 | b.backend.SendMessage(text, channel)
151 | }
152 |
153 | func (b *Bot) Listen() {
154 | b.backend.Listen(b)
155 | }
156 |
157 | // NewTestBot returns a bot for testing
158 | func NewTestBot() (*Bot, error) {
159 | backend := &SlackBotBackend{}
160 | return NewBot(NewConfig(true, false), "testbot", "", backend), nil
161 | }
162 |
163 | // BlockQuote returns the string block-quoted
164 | func BlockQuote(s string) string {
165 | if !strings.HasSuffix(s, "\n") {
166 | s += "\n"
167 | }
168 | return "```\n" + s + "```"
169 | }
170 |
171 | // GetTokenFromEnv returns slack token from the environment
172 | func GetTokenFromEnv() string {
173 | token := os.Getenv("SLACK_TOKEN")
174 | if token == "" {
175 | log.Fatal("SLACK_TOKEN is not set")
176 | }
177 | return token
178 | }
179 |
180 | func isSpace(r rune) bool {
181 | switch r {
182 | case ' ', '\t', '\r', '\n':
183 | return true
184 | }
185 | return false
186 | }
187 |
188 | func parseInput(s string) []string {
189 | buf := ""
190 | args := []string{}
191 | var escaped, doubleQuoted, singleQuoted bool
192 | for _, r := range s {
193 | if escaped {
194 | buf += string(r)
195 | escaped = false
196 | continue
197 | }
198 |
199 | if r == '\\' {
200 | if singleQuoted {
201 | buf += string(r)
202 | } else {
203 | escaped = true
204 | }
205 | continue
206 | }
207 |
208 | if isSpace(r) {
209 | if singleQuoted || doubleQuoted {
210 | buf += string(r)
211 | } else if buf != "" {
212 | args = append(args, buf)
213 | buf = ""
214 | }
215 | continue
216 | }
217 |
218 | switch r {
219 | case '"':
220 | if !singleQuoted {
221 | doubleQuoted = !doubleQuoted
222 | continue
223 | }
224 | case '\'':
225 | if !doubleQuoted {
226 | singleQuoted = !singleQuoted
227 | continue
228 | }
229 | }
230 |
231 | buf += string(r)
232 | }
233 | if buf != "" {
234 | args = append(args, buf)
235 | }
236 | return args
237 | }
238 |
--------------------------------------------------------------------------------
/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 := filepath.Join(e.Home, "Library", "LaunchAgents")
180 | //nolint:gosec // LaunchAgents directory must be world-readable for launchd
181 | if err := os.MkdirAll(plistDir, 0o755); err != nil {
182 | return "", err
183 | }
184 | path := filepath.Clean(filepath.Join(plistDir, script.Label+".plist"))
185 | log.Printf("Writing %s", path)
186 | //nolint:gosec // Plist files must be readable by launchd
187 | if err := os.WriteFile(path, data, 0o755); err != nil {
188 | return "", err
189 | }
190 | return path, nil
191 | }
192 |
193 | // Cleanup removes any files generated by Env
194 | func (e Env) Cleanup(script Script) error {
195 | plistDir := e.Home + "/Library/LaunchAgents"
196 | path := fmt.Sprintf("%s/%s.plist", plistDir, script.Label)
197 | log.Printf("Removing %s", path)
198 | return os.Remove(path)
199 | }
200 |
201 | // CleanupLog removes log path
202 | func CleanupLog(env Env, label string) error {
203 | // Remove log
204 | logPath, lerr := env.LogPathForLaunchdLabel(label)
205 | if lerr != nil {
206 | return lerr
207 | }
208 | if _, err := os.Stat(logPath); err == nil {
209 | if err := os.Remove(logPath); err != nil {
210 | return err
211 | }
212 | }
213 | return nil
214 | }
215 |
--------------------------------------------------------------------------------
/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 | javaHome := "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home"
92 | javaBin := javaHome + "/bin"
93 | // need custom go to fix issue
94 | goRoot := "/Users/build/code/go"
95 | goBin := goRoot + "/bin"
96 | path := goBin + ":" + javaBin + ":/sbin:/usr/sbin:/bin:/usr/local/bin:/usr/bin:/opt/homebrew/bin"
97 | env := launchd.NewEnv(home, path)
98 | androidHome := "/usr/local/opt/android-sdk"
99 | // ndkVer65x := "23.1.7779620"
100 | ndkVer66x := "26.1.10909125"
101 | ndkVer := ndkVer66x
102 | NDKPath := "/Users/build/Library/Android/sdk/ndk/" + ndkVer
103 |
104 | switch cmd {
105 | case cancel.FullCommand():
106 | if *cancelLabel == "" {
107 | return "Label required for cancel", errors.New("Label required for cancel")
108 | }
109 | return launchd.Stop(*cancelLabel)
110 |
111 | case buildDarwin.FullCommand():
112 | smokeTest := true
113 | skipCI := *buildDarwinSkipCI
114 | testBuild := *buildDarwinTest
115 | // If it's a custom build, make it a test build unless --smoke is passed.
116 | if *buildDarwinClientCommit != "" || *buildDarwinKbfsCommit != "" {
117 | smokeTest = *buildDarwinSmoke
118 | testBuild = !*buildDarwinSmoke
119 | }
120 | script := launchd.Script{
121 | Label: "keybase.build.darwin",
122 | Path: "github.com/keybase/client/packaging/build_darwin.sh",
123 | BucketName: "prerelease.keybase.io",
124 | Platform: "darwin",
125 | EnvVars: []launchd.EnvVar{
126 | {Key: "SMOKE_TEST", Value: boolToEnvString(smokeTest)},
127 | {Key: "TEST", Value: boolToEnvString(testBuild)},
128 | {Key: "CLIENT_COMMIT", Value: *buildDarwinClientCommit},
129 | {Key: "KBFS_COMMIT", Value: *buildDarwinKbfsCommit},
130 | // TODO: Rename to SKIP_CI in packaging scripts
131 | {Key: "NOWAIT", Value: boolToEnvString(skipCI)},
132 | {Key: "NOPULL", Value: boolToEnvString(*buildDarwinNoPull)},
133 | {Key: "NOS3", Value: boolToEnvString(*buildDarwinNoS3)},
134 | {Key: "NONOTARIZE", Value: boolToEnvString(*buildDarwinNoNotarize)},
135 | },
136 | }
137 | return runScript(bot, channel, env, script)
138 |
139 | case buildMobile.FullCommand():
140 | skipCI := *buildMobileSkipCI
141 | automated := *buildMobileAutomated
142 | script := launchd.Script{
143 | Label: "keybase.build.mobile",
144 | Path: "github.com/keybase/client/packaging/build_mobile.sh",
145 | BucketName: "prerelease.keybase.io",
146 | EnvVars: []launchd.EnvVar{
147 | {Key: "ANDROID_HOME", Value: androidHome},
148 | {Key: "ANDROID_SDK", Value: androidHome},
149 | {Key: "ANDROID_SDK_ROOT", Value: androidHome},
150 | {Key: "ANDROID_NDK_HOME", Value: NDKPath},
151 | {Key: "NDK_HOME", Value: NDKPath},
152 | {Key: "ANDROID_NDK", Value: NDKPath},
153 | {Key: "CLIENT_COMMIT", Value: *buildMobileCientCommit},
154 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)},
155 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)},
156 | },
157 | }
158 | env.GoPath = env.PathFromHome("go-ios")
159 | return runScript(bot, channel, env, script)
160 |
161 | case buildAndroid.FullCommand():
162 | skipCI := *buildAndroidSkipCI
163 | automated := *buildAndroidAutomated
164 | script := launchd.Script{
165 | Label: "keybase.build.android",
166 | Path: "github.com/keybase/client/packaging/android/build_and_publish.sh",
167 | BucketName: "prerelease.keybase.io",
168 | EnvVars: []launchd.EnvVar{
169 | {Key: "ANDROID_HOME", Value: androidHome},
170 | {Key: "ANDROID_NDK_HOME", Value: NDKPath},
171 | {Key: "ANDROID_NDK", Value: NDKPath},
172 | {Key: "CLIENT_COMMIT", Value: *buildAndroidCientCommit},
173 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)},
174 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)},
175 | },
176 | }
177 | env.GoPath = env.PathFromHome("go-android") // Custom go path for Android so we don't conflict
178 | return runScript(bot, channel, env, script)
179 |
180 | case buildIOS.FullCommand():
181 | skipCI := *buildIOSSkipCI
182 | iosClean := *buildIOSClean
183 | automated := *buildIOSAutomated
184 | script := launchd.Script{
185 | Label: "keybase.build.ios",
186 | Path: "github.com/keybase/client/packaging/ios/build_and_publish.sh",
187 | BucketName: "prerelease.keybase.io",
188 | EnvVars: []launchd.EnvVar{
189 | {Key: "CLIENT_COMMIT", Value: *buildIOSCientCommit},
190 | {Key: "CLEAN", Value: boolToEnvString(iosClean)},
191 | {Key: "CHECK_CI", Value: boolToEnvString(!skipCI)},
192 | {Key: "AUTOMATED_BUILD", Value: boolToEnvString(automated)},
193 | },
194 | }
195 | env.GoPath = env.PathFromHome("go-ios") // Custom go path for iOS so we don't conflict
196 | return runScript(bot, channel, env, script)
197 |
198 | case releasePromote.FullCommand():
199 | script := launchd.Script{
200 | Label: "keybase.release.promote",
201 | Path: "github.com/keybase/slackbot/scripts/release.promote.sh",
202 | BucketName: "prerelease.keybase.io",
203 | Platform: *releaseToPromotePlatform,
204 | EnvVars: []launchd.EnvVar{
205 | {Key: "RELEASE_TO_PROMOTE", Value: *releaseToPromote},
206 | {Key: "DRY_RUN", Value: boolToString(*releaseToPromoteDryRun)},
207 | },
208 | }
209 | return runScript(bot, channel, env, script)
210 |
211 | case dumplogCmd.FullCommand():
212 | readPath, err := env.LogPathForLaunchdLabel(*dumplogCommandLabel)
213 | if err != nil {
214 | return "", err
215 | }
216 | script := launchd.Script{
217 | Label: "keybase.dumplog",
218 | Path: "github.com/keybase/slackbot/scripts/dumplog.sh",
219 | BucketName: "prerelease.keybase.io",
220 | EnvVars: []launchd.EnvVar{
221 | {Key: "READ_PATH", Value: readPath},
222 | {Key: "NOLOG", Value: boolToEnvString(true)},
223 | },
224 | }
225 | return runScript(bot, channel, env, script)
226 |
227 | case gitDiffCmd.FullCommand():
228 | rawRepoText := *gitDiffRepo
229 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1]
230 |
231 | script := launchd.Script{
232 | Label: "keybase.gitdiff",
233 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh",
234 | BucketName: "prerelease.keybase.io",
235 | EnvVars: []launchd.EnvVar{
236 | {Key: "REPO", Value: repoParsed},
237 | {Key: "PREFIX_GOPATH", Value: boolToEnvString(true)},
238 | {Key: "SCRIPT_TO_RUN", Value: "./git_diff.sh"},
239 | },
240 | }
241 | return runScript(bot, channel, env, script)
242 |
243 | case gitCleanCmd.FullCommand():
244 | script := launchd.Script{
245 | Label: "keybase.gitclean",
246 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh",
247 | BucketName: "prerelease.keybase.io",
248 | EnvVars: []launchd.EnvVar{
249 | {Key: "SCRIPT_TO_RUN", Value: "./git_clean.sh"},
250 | },
251 | }
252 | return runScript(bot, channel, env, script)
253 |
254 | case nodeModuleCleanCmd.FullCommand():
255 | script := launchd.Script{
256 | Label: "keybase.nodeModuleClean",
257 | Path: "github.com/keybase/slackbot/scripts/run_and_send_stdout.sh",
258 | BucketName: "prerelease.keybase.io",
259 | EnvVars: []launchd.EnvVar{
260 | {Key: "SCRIPT_TO_RUN", Value: "./node_module_clean.sh"},
261 | },
262 | }
263 | return runScript(bot, channel, env, script)
264 |
265 | case releaseBroken.FullCommand():
266 | script := launchd.Script{
267 | Label: "keybase.release.broken",
268 | Path: "github.com/keybase/slackbot/scripts/release.broken.sh",
269 | BucketName: "prerelease.keybase.io",
270 | Platform: "darwin",
271 | EnvVars: []launchd.EnvVar{
272 | {Key: "BROKEN_RELEASE", Value: *releaseBrokenVersion},
273 | },
274 | }
275 | return runScript(bot, channel, env, script)
276 |
277 | case smoketest.FullCommand():
278 | script := launchd.Script{
279 | Label: "keybase.smoketest",
280 | Path: "github.com/keybase/slackbot/scripts/smoketest.sh",
281 | BucketName: "prerelease.keybase.io",
282 | Platform: *smoketestPlatform,
283 | EnvVars: []launchd.EnvVar{
284 | {Key: "SMOKETEST_BUILD_A", Value: *smoketestBuildA},
285 | {Key: "SMOKETEST_MAX_TESTERS", Value: strconv.Itoa(*smoketestMaxTesters)},
286 | {Key: "SMOKETEST_ENABLE", Value: boolToString(*smoketestEnable)},
287 | },
288 | }
289 | return runScript(bot, channel, env, script)
290 |
291 | case upgrade.FullCommand():
292 | script := launchd.Script{
293 | Label: "keybase.update",
294 | Path: "github.com/keybase/slackbot/scripts/upgrade.sh",
295 | EnvVars: []launchd.EnvVar{
296 | {Key: "NAME", Value: *upgradePackageName},
297 | },
298 | }
299 | return runScript(bot, channel, env, script)
300 | }
301 |
302 | return cmd, nil
303 | }
304 |
305 | func (k *keybot) Help(bot *slackbot.Bot) string {
306 | out, err := k.Run(bot, "", nil)
307 | if err != nil {
308 | return fmt.Sprintf("Error getting help: %s", err)
309 | }
310 | return out
311 | }
312 |
--------------------------------------------------------------------------------
/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 | "log"
11 | "os"
12 | "os/exec"
13 | "path"
14 | "path/filepath"
15 | "strings"
16 | "sync"
17 | "time"
18 |
19 | "github.com/keybase/slackbot"
20 | "github.com/keybase/slackbot/cli"
21 | kingpin "gopkg.in/alecthomas/kingpin.v2"
22 | )
23 |
24 | type winbot struct {
25 | testAuto chan struct{}
26 | stopAuto chan struct{}
27 | }
28 |
29 | const numLogLines = 10
30 |
31 | // Keep track of the current build process, protected with a mutex,
32 | // to support cancellation
33 | var (
34 | buildProcessMutex sync.Mutex
35 | buildProcess *os.Process
36 | )
37 |
38 | func (d *winbot) Run(bot *slackbot.Bot, channel string, args []string) (string, error) {
39 | app := kingpin.New("winbot", "Job command parser for winbot")
40 | app.Terminate(nil)
41 | stringBuffer := new(bytes.Buffer)
42 | app.Writer(stringBuffer)
43 |
44 | buildWindows := app.Command("build", "Start a windows build")
45 | buildWindowsTest := buildWindows.Flag("test", "Test build, skips admin/test channel").Bool()
46 | buildWindowsCientCommit := buildWindows.Flag("client-commit", "Build a specific client commit").String()
47 | buildWindowsKbfsCommit := buildWindows.Flag("kbfs-commit", "Build a specific kbfs commit").String()
48 | buildWindowsUpdaterCommit := buildWindows.Flag("updater-commit", "Build a specific updater commit").String()
49 | buildWindowsSkipCI := buildWindows.Flag("skip-ci", "Whether to skip CI").Bool()
50 | buildWindowsSmoke := buildWindows.Flag("smoke", "Build a smoke pair").Bool()
51 | buildWindowsDevCert := buildWindows.Flag("dev-cert", "Build using devel code signing cert").Bool()
52 | buildWindowsAuto := buildWindows.Flag("automated", "Specify build was triggered automatically").Hidden().Bool()
53 |
54 | cancel := app.Command("cancel", "Cancel current")
55 |
56 | dumplogCmd := app.Command("dumplog", "Show the last log file")
57 | gitDiffCmd := app.Command("gdiff", "Show the git diff")
58 | gitDiffRepo := gitDiffCmd.Arg("repo", "Repo path relative to $GOPATH/src").Required().String()
59 |
60 | gitCleanCmd := app.Command("gclean", "Clean the repo")
61 | gitCleanRepo := gitCleanCmd.Arg("repo", "Repo path relative to $GOPATH/src").Required().String()
62 |
63 | logFileName := filepath.Clean(path.Join(os.TempDir(), "keybase.build.windows.log"))
64 |
65 | testAutoBuild := app.Command("testauto", "Simulate an automated daily build").Hidden()
66 | startAutoTimer := app.Command("startAutoTimer", "Start the auto build timer")
67 | startAutoTimerInterval := startAutoTimer.Flag("interval", "Number of hours between auto builds, 0 to stop").Default("24").Int()
68 | startAutoTimerStartHour := startAutoTimer.Flag("startHour", "Number of hours after midnight to build, local time").Default("7").Int()
69 | startAutoTimerDelay := startAutoTimer.Flag("delay", "Number of hours to wait before starting auto timer").Default("0").Int()
70 |
71 | restartCmd := app.Command("restart", "Quit and let calling script invoke bot again")
72 |
73 | cmd, usage, cmdErr := cli.Parse(app, args, stringBuffer)
74 | if usage != "" || cmdErr != nil {
75 | return usage, cmdErr
76 | }
77 |
78 | // do these regardless of dry run status
79 | if cmd == testAutoBuild.FullCommand() {
80 | d.testAuto <- struct{}{}
81 | return "Sent test signal", nil
82 | }
83 |
84 | if cmd == startAutoTimer.FullCommand() {
85 | if d.stopAuto != nil {
86 | d.stopAuto <- struct{}{}
87 | }
88 | if *startAutoTimerInterval > 0 {
89 | go d.winAutoBuild(bot, channel, *startAutoTimerInterval, *startAutoTimerDelay, *startAutoTimerStartHour)
90 | }
91 | return "", nil
92 | }
93 |
94 | if bot.Config().DryRun() {
95 | return fmt.Sprintf("I would have run: `%#v`", cmd), nil
96 | }
97 |
98 | switch cmd {
99 | case cancel.FullCommand():
100 | buildProcessMutex.Lock()
101 | defer buildProcessMutex.Unlock()
102 | if buildProcess == nil {
103 | return "No build running", nil
104 | }
105 | if err := buildProcess.Kill(); err != nil {
106 | return "failed to cancel build", err
107 | }
108 |
109 | case buildWindows.FullCommand():
110 | smokeTest := *buildWindowsSmoke
111 | skipCI := *buildWindowsSkipCI
112 | skipTestChannel := *buildWindowsTest
113 | devCert := 0
114 | if *buildWindowsDevCert {
115 | devCert = 1
116 | }
117 | var autoBuild string
118 |
119 | if bot.Config().DryRun() {
120 | return "I would have done a build", nil
121 | }
122 |
123 | if bot.Config().Paused() {
124 | return "I'm paused so I can't do that, but I would have done a build", nil
125 | }
126 |
127 | if *buildWindowsAuto {
128 | autoBuild = "Automatic Build: "
129 | }
130 |
131 | // Test channel tells the scripts this is an admin build
132 | updateChannel := "Test"
133 | if skipTestChannel {
134 | if smokeTest {
135 | return "Test and Smoke are exclusive options", nil
136 | }
137 | updateChannel = "None"
138 | } else if smokeTest {
139 | updateChannel = "Smoke"
140 | if !skipCI {
141 | updateChannel = "SmokeCI"
142 | }
143 | }
144 |
145 | msg := fmt.Sprintf(autoBuild+"I'm starting the job `windows build`. To cancel run `!%s cancel`. ", bot.Name())
146 | msg = fmt.Sprintf(msg+"updateChannel is %s, smokeTest is %v, devCert is %v, logFileName %s",
147 | updateChannel, smokeTest, devCert, logFileName)
148 | bot.SendMessage(msg, channel)
149 |
150 | if err := os.Remove(logFileName); err != nil && !os.IsNotExist(err) {
151 | return "Unable to remove old logfile", err
152 | }
153 | logf, err := os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE, 0o600)
154 | if err != nil {
155 | return "Unable to open logfile", err
156 | }
157 |
158 | //nolint:noctx // Long-running git operation, no request context available
159 | gitCmd := exec.Command(
160 | "git.exe",
161 | "checkout",
162 | "master",
163 | )
164 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
165 | stdoutStderr, err := gitCmd.CombinedOutput()
166 | if _, writeErr := logf.Write(stdoutStderr); writeErr != nil {
167 | log.Printf("Error writing to log: %s", writeErr)
168 | }
169 | if err != nil {
170 | if _, writeErr := logf.WriteString(gitCmd.Dir); writeErr != nil {
171 | log.Printf("Error writing dir to log: %s", writeErr)
172 | }
173 | if closeErr := logf.Close(); closeErr != nil {
174 | log.Printf("Error closing log: %s", closeErr)
175 | }
176 | return string(stdoutStderr), err
177 | }
178 |
179 | //nolint:noctx // Long-running git operation, no request context available
180 | gitCmd = exec.Command(
181 | "git.exe",
182 | "pull",
183 | )
184 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
185 | stdoutStderr, err = gitCmd.CombinedOutput()
186 | if _, writeErr := logf.Write(stdoutStderr); writeErr != nil {
187 | log.Printf("Error writing to log: %s", writeErr)
188 | }
189 | if err != nil {
190 | if _, writeErr := logf.WriteString(gitCmd.Dir); writeErr != nil {
191 | log.Printf("Error writing dir to log: %s", writeErr)
192 | }
193 | if closeErr := logf.Close(); closeErr != nil {
194 | log.Printf("Error closing log: %s", closeErr)
195 | }
196 | return string(stdoutStderr), err
197 | }
198 |
199 | if buildWindowsCientCommit != nil && *buildWindowsCientCommit != "" && *buildWindowsCientCommit != "master" {
200 | msg := fmt.Sprintf(autoBuild+"I'm trying to use commit %s", *buildWindowsCientCommit)
201 | bot.SendMessage(msg, channel)
202 |
203 | //nolint:gosec,noctx // Checking out user-specified commit, no request context available
204 | gitCmd = exec.Command(
205 | "git.exe",
206 | "checkout",
207 | *buildWindowsCientCommit,
208 | )
209 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
210 | stdoutStderr, err = gitCmd.CombinedOutput()
211 | if _, writeErr := logf.Write(stdoutStderr); writeErr != nil {
212 | log.Printf("Error writing to log: %s", writeErr)
213 | }
214 |
215 | if err != nil {
216 | if _, writeErr := fmt.Fprintf(logf, "error doing git pull in %s\n", gitCmd.Dir); writeErr != nil {
217 | log.Printf("Error writing error to log: %s", writeErr)
218 | }
219 | if closeErr := logf.Close(); closeErr != nil {
220 | log.Printf("Error closing log: %s", closeErr)
221 | }
222 | return string(stdoutStderr), err
223 | }
224 |
225 | // Test if we're on a branch. If so, do git pull once more.
226 | //nolint:noctx // Long-running git operation, no request context available
227 | gitCmd = exec.Command(
228 | "git.exe",
229 | "rev-parse",
230 | "--abbrev-ref",
231 | "HEAD",
232 | )
233 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
234 | stdoutStderr, err = gitCmd.CombinedOutput()
235 | if err != nil {
236 | if _, writeErr := fmt.Fprintf(logf, "error going git rev-parse dir: %s\n", gitCmd.Dir); writeErr != nil {
237 | log.Printf("Error writing error to log: %s", writeErr)
238 | }
239 | if closeErr := logf.Close(); closeErr != nil {
240 | log.Printf("Error closing log: %s", closeErr)
241 | }
242 | return string(stdoutStderr), err
243 | }
244 | commit := strings.TrimSpace(string(stdoutStderr))
245 | if commit != "HEAD" {
246 | //nolint:noctx // Long-running git operation, no request context available
247 | gitCmd = exec.Command(
248 | "git.exe",
249 | "pull",
250 | )
251 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
252 | stdoutStderr, err = gitCmd.CombinedOutput()
253 | if _, writeErr := logf.Write(stdoutStderr); writeErr != nil {
254 | log.Printf("Error writing to log: %s", writeErr)
255 | }
256 | if err != nil {
257 | if _, writeErr := fmt.Fprintf(logf, "error doing git pull on %s in %s\n", commit, gitCmd.Dir); writeErr != nil {
258 | log.Printf("Error writing error to log: %s", writeErr)
259 | }
260 | if closeErr := logf.Close(); closeErr != nil {
261 | log.Printf("Error closing log: %s", closeErr)
262 | }
263 | return string(stdoutStderr), err
264 | }
265 | }
266 | }
267 |
268 | //nolint:noctx // Long-running git operation, no request context available
269 | gitCmd = exec.Command(
270 | "git.exe",
271 | "rev-parse",
272 | "HEAD",
273 | )
274 | gitCmd.Dir = os.ExpandEnv("$GOPATH/src/github.com/keybase/client")
275 | stdoutStderr, err = gitCmd.CombinedOutput()
276 | if err != nil {
277 | if _, writeErr := fmt.Fprintf(logf, "error getting current commit for logs: %s", gitCmd.Dir); writeErr != nil {
278 | log.Printf("Error writing error to log: %s", writeErr)
279 | }
280 | if closeErr := logf.Close(); closeErr != nil {
281 | log.Printf("Error closing log: %s", closeErr)
282 | }
283 | return string(stdoutStderr), err
284 | }
285 | if _, writeErr := fmt.Fprintf(logf, "HEAD is currently at %s\n", string(stdoutStderr)); writeErr != nil {
286 | log.Printf("Error writing to log: %s", writeErr)
287 | }
288 |
289 | //nolint:gosec,noctx // Build script execution from known location in GOPATH, no request context available
290 | cmd := exec.Command(
291 | "cmd", "/c",
292 | path.Join(os.Getenv("GOPATH"), "src/github.com/keybase/client/packaging/windows/dorelease.cmd"),
293 | ">>",
294 | logFileName,
295 | "2>&1")
296 | cmd.Env = append(os.Environ(),
297 | "ClientRevision="+*buildWindowsCientCommit,
298 | "KbfsRevision="+*buildWindowsKbfsCommit,
299 | "UpdaterRevision="+*buildWindowsUpdaterCommit,
300 | "UpdateChannel="+updateChannel,
301 | fmt.Sprintf("DevCert=%d", devCert),
302 | "SlackBot=1",
303 | )
304 | if _, writeErr := fmt.Fprintf(logf, "cmd: %+v\n", cmd); writeErr != nil {
305 | log.Printf("Error writing cmd to log: %s", writeErr)
306 | }
307 | if closeErr := logf.Close(); closeErr != nil {
308 | log.Printf("Error closing log: %s", closeErr)
309 | }
310 |
311 | go func() {
312 | err := cmd.Start()
313 | if err != nil {
314 | bot.SendMessage(fmt.Sprintf("unable to start: %s", err), channel)
315 | }
316 | buildProcessMutex.Lock()
317 | buildProcess = cmd.Process
318 | buildProcessMutex.Unlock()
319 | err = cmd.Wait()
320 |
321 | bucketName := os.Getenv("BUCKET_NAME")
322 | if bucketName == "" {
323 | bucketName = "prerelease.keybase.io"
324 | }
325 | //nolint:gosec,noctx // Executing release tool from known location in GOPATH with safe arguments, no context available
326 | sendLogCmd := exec.Command(
327 | path.Join(os.Getenv("GOPATH"), "src/github.com/keybase/client/go/release/release.exe"),
328 | "save-log",
329 | "--maxsize=5000000",
330 | "--bucket-name="+bucketName,
331 | "--path="+logFileName,
332 | )
333 | resultMsg := autoBuild + "Finished the job `windows build`"
334 | if err != nil {
335 | resultMsg = autoBuild + "Error in job `windows build`"
336 | var lines [numLogLines]string
337 | // Send a log snippet too
338 | index := 0
339 | lineCount := 0
340 |
341 | f, err := os.Open(logFileName)
342 | if err != nil {
343 | bot.SendMessage(autoBuild+"Error reading "+logFileName+": "+err.Error(), channel)
344 | }
345 |
346 | scanner := bufio.NewScanner(f)
347 | for scanner.Scan() {
348 | lines[lineCount%numLogLines] = scanner.Text()
349 | lineCount++
350 | }
351 | if err := scanner.Err(); err != nil {
352 | bot.SendMessage(autoBuild+"Error scanning "+logFileName+": "+err.Error(), channel)
353 | }
354 | if lineCount > numLogLines {
355 | index = lineCount % numLogLines
356 | lineCount = numLogLines
357 | }
358 | snippet := "```\n"
359 | for i := 0; i < lineCount; i++ {
360 | snippet += lines[(i+index)%numLogLines] + "\n"
361 | }
362 | snippet += "```"
363 | bot.SendMessage(snippet, channel)
364 | }
365 | urlBytes, err2 := sendLogCmd.Output()
366 | if err2 != nil {
367 | msg := fmt.Sprintf("%s, log upload error %s", resultMsg, err2.Error())
368 | bot.SendMessage(msg, channel)
369 | } else {
370 | msg := fmt.Sprintf("%s, view log at %s", resultMsg, string(urlBytes))
371 | bot.SendMessage(msg, channel)
372 | }
373 | }()
374 | return "", nil
375 | case dumplogCmd.FullCommand():
376 | logContents, err := os.ReadFile(logFileName)
377 | if err != nil {
378 | return "Error reading " + logFileName, err
379 | }
380 | index := 0
381 | if len(logContents) > 1000 {
382 | index = len(logContents) - 1000
383 | }
384 | bot.SendMessage(string(logContents[index:]), channel)
385 |
386 | case gitDiffCmd.FullCommand():
387 | rawRepoText := *gitDiffRepo
388 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1]
389 |
390 | //nolint:noctx // User-initiated git command, no request context available
391 | gitDiffCmd := exec.Command(
392 | "git.exe",
393 | "diff",
394 | )
395 | gitDiffCmd.Dir = os.ExpandEnv(path.Join("$GOPATH/src", repoParsed))
396 |
397 | if exists, err := Exists(path.Join(gitDiffCmd.Dir, ".git")); !exists {
398 | return "Not a git repo", err
399 | }
400 |
401 | stdoutStderr, err := gitDiffCmd.CombinedOutput()
402 | if err != nil {
403 | return "Error", err
404 | }
405 | bot.SendMessage(string(stdoutStderr), channel)
406 |
407 | case gitCleanCmd.FullCommand():
408 | rawRepoText := *gitCleanRepo
409 | repoParsed := strings.Split(strings.Trim(rawRepoText, "`<>"), "|")[1]
410 |
411 | //nolint:noctx // User-initiated git command, no request context available
412 | gitCleanCmd := exec.Command(
413 | "git.exe",
414 | "clean",
415 | "-f",
416 | )
417 | gitCleanCmd.Dir = os.ExpandEnv(path.Join("$GOPATH/src", repoParsed))
418 |
419 | if exists, err := Exists(path.Join(gitCleanCmd.Dir, ".git")); !exists {
420 | return "Not a git repo", err
421 | }
422 |
423 | stdoutStderr, err := gitCleanCmd.CombinedOutput()
424 | if err != nil {
425 | return "Error", err
426 | }
427 |
428 | bot.SendMessage(string(stdoutStderr), channel)
429 |
430 | case restartCmd.FullCommand():
431 | os.Exit(0) //nolint
432 | }
433 | return cmd, nil
434 | }
435 |
436 | func (d *winbot) Help(bot *slackbot.Bot) string {
437 | out, err := d.Run(bot, "", nil)
438 | if err != nil {
439 | return fmt.Sprintf("Error getting help: %s", err)
440 | }
441 | return out
442 | }
443 |
444 | func Exists(name string) (bool, error) {
445 | _, err := os.Stat(name)
446 | if os.IsNotExist(err) {
447 | return false, nil
448 | }
449 | return err != nil, err
450 | }
451 |
452 | func (d *winbot) winAutoBuild(bot *slackbot.Bot, channel string, interval int, delay int, startHour int) {
453 | d.testAuto = make(chan struct{})
454 | d.stopAuto = make(chan struct{})
455 | for {
456 | hour := time.Now().Hour() + delay
457 | if delay > 0 {
458 | delay = 0
459 | } else {
460 | hour = ((interval - hour) + startHour)
461 | }
462 | next := time.Now().Add(time.Hour * time.Duration(hour))
463 | for next.Weekday() == time.Saturday || next.Weekday() == time.Sunday {
464 | hour += interval
465 | next = time.Now().Add(time.Hour * time.Duration(hour))
466 | }
467 |
468 | msg := fmt.Sprintf("Next automatic build at %s", next.Format(time.RFC822))
469 | bot.SendMessage(msg, channel)
470 |
471 | args := []string{"build", "--automated"}
472 |
473 | select {
474 | case <-d.testAuto:
475 | case <-time.After(time.Duration(hour) * time.Hour):
476 | args = append(args, "--smoke")
477 | case <-d.stopAuto:
478 | return
479 | }
480 | message, err := d.Run(bot, channel, args)
481 | if err != nil {
482 | msg := fmt.Sprintf("AutoBuild ERROR -- %s: %s", message, err.Error())
483 | bot.SendMessage(msg, channel)
484 | }
485 | }
486 | }
487 |
--------------------------------------------------------------------------------