From 6d8745c36d8f6224ef1399d0c2b9d7441f80b024 Mon Sep 17 00:00:00 2001 From: Anil Vishnoi Date: Mon, 13 May 2024 02:11:41 -0700 Subject: [PATCH] Add UI for submitting the PR for skill and knowledge - Adds UX Forms for skill and knowledge data collection - Push the user provided data to the gobot through apiserver - Gobot takes the data and generate PR to the taxonomy repo. - Gobot make sure all the linting rules are taken care, attribution file is properly generated and commits are signed off. Signed-off-by: Anil Vishnoi --- .ansible-lint | 1 + .env.example | 3 + .vscode/settings.json | 2 + deploy/compose/dev-single-worker-with-ui.yaml | 3 + gobot/cmd/root.go | 24 +- gobot/go.mod | 26 +- gobot/go.sum | 121 ++++- ...ssue_comment.go => issue_comment_event.go} | 0 gobot/handlers/pull_request_create.go | 483 ++++++++++++++++++ ...{pull_request.go => pull_request_event.go} | 6 +- gobot/handlers/yaml_util.go | 139 +++++ ui/apiserver/apiserver.go | 153 +++++- ui/package-lock.json | 7 + ui/package.json | 1 + ui/src/app/Contribute/Knowledge/Knowledge.css | 83 +++ ui/src/app/Contribute/Knowledge/Knowledge.tsx | 372 ++++++++++++++ ui/src/app/Contribute/Skill/Skill.css | 74 +++ ui/src/app/Contribute/Skill/Skill.tsx | 329 ++++++++++++ ui/src/app/common/HooksPostKnowledgePR.tsx | 65 +++ ui/src/app/common/HooksPostSkillPR.tsx | 60 +++ ui/src/app/routes.tsx | 20 + 21 files changed, 1955 insertions(+), 17 deletions(-) rename gobot/handlers/{issue_comment.go => issue_comment_event.go} (100%) create mode 100644 gobot/handlers/pull_request_create.go rename gobot/handlers/{pull_request.go => pull_request_event.go} (92%) create mode 100644 gobot/handlers/yaml_util.go create mode 100644 ui/src/app/Contribute/Knowledge/Knowledge.css create mode 100644 ui/src/app/Contribute/Knowledge/Knowledge.tsx create mode 100644 ui/src/app/Contribute/Skill/Skill.css create mode 100644 ui/src/app/Contribute/Skill/Skill.tsx create mode 100644 ui/src/app/common/HooksPostKnowledgePR.tsx create mode 100644 ui/src/app/common/HooksPostSkillPR.tsx diff --git a/.ansible-lint b/.ansible-lint index cfa7a802..6fbec00d 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -1,3 +1,4 @@ exclude_paths: - compose.yaml - .github/ + - ui/ diff --git a/.env.example b/.env.example index 10d92781..8754f8df 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,9 @@ ILBOT_WEBHOOK_PROXY_URL=https://smee.io/your-webhook-proxy-url # Github App Id after bot's registration. ILBOT_GITHUB_INTEGRATION_ID=your-github-app-id +# Taxonomy URL +ILBOT_TAXONOMY_REPO= + # Webhook secret created during bot registration ILBOT_GITHUB_WEBHOOK_SECRET=your-webhook-secret diff --git a/.vscode/settings.json b/.vscode/settings.json index 424441a6..a9ba1582 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,8 @@ "deploy/ansible/*.yml": "ansible" }, "cSpell.words": [ + "githttp", + "Pallinder", "triagers" ] } diff --git a/deploy/compose/dev-single-worker-with-ui.yaml b/deploy/compose/dev-single-worker-with-ui.yaml index dbc8e302..809c3482 100644 --- a/deploy/compose/dev-single-worker-with-ui.yaml +++ b/deploy/compose/dev-single-worker-with-ui.yaml @@ -14,6 +14,9 @@ services: - ../../.env depends_on: - redis + ports: + - 8081:8081 + bot-ui: container_name: ui diff --git a/gobot/cmd/root.go b/gobot/cmd/root.go index 5b219786..3effe68b 100644 --- a/gobot/cmd/root.go +++ b/gobot/cmd/root.go @@ -38,10 +38,13 @@ var ( HTTPAddress string HTTPPort int GithubIntegrationID int + TaxonomyRepo string GithubURL string GithubWebhookSecret string GithubAppPrivateKey string WebhookProxyURL string + GithubUsername string + GithubToken string RequiredLabels []string Maintainers []string BotUsername string @@ -51,12 +54,15 @@ var ( func init() { rootCmd.PersistentFlags().StringVarP(&RedisHost, "redis", "", "redis:6379", "The Redis instance to connect to") rootCmd.PersistentFlags().StringVarP(&HTTPAddress, "http-address", "", "127.0.0.1", "HTTP Address to bind to") - rootCmd.PersistentFlags().IntVarP(&HTTPPort, "http-port", "", 8080, "HTTP Port to bind to") + rootCmd.PersistentFlags().IntVarP(&HTTPPort, "http-port", "", 8081, "HTTP Port to bind to") rootCmd.PersistentFlags().IntVarP(&GithubIntegrationID, "github-integration-id", "", 0, "The GitHub App Integration ID") + rootCmd.PersistentFlags().StringVarP(&TaxonomyRepo, "taxonomy-repo", "", "https://github.com/instructlab/taxonomy.git", "The GitHub repository to use for the taxonomy") rootCmd.PersistentFlags().StringVarP(&GithubURL, "github-url", "", "https://api.github.com/", "The URL of the GitHub instance") rootCmd.PersistentFlags().StringVarP(&GithubWebhookSecret, "github-webhook-secret", "", "", "The GitHub App Webhook Secret") rootCmd.PersistentFlags().StringVarP(&GithubAppPrivateKey, "github-app-private-key", "", "", "The GitHub App Private Key") rootCmd.PersistentFlags().StringVarP(&WebhookProxyURL, "webhook-proxy-url", "", "", "Get an ID from https://smee.io/new. If blank, the app will not use a webhook proxy") + rootCmd.PersistentFlags().StringVarP(&GithubUsername, "github-username", "u", "instructlab-bot", "The GitHub username to use for authentication") + rootCmd.PersistentFlags().StringVarP(&GithubToken, "github-token", "g", "", "The GitHub token to use for authentication") rootCmd.PersistentFlags().StringSliceVarP(&RequiredLabels, "required-labels", "", []string{}, "Label(s) required before a PR can be tested") rootCmd.PersistentFlags().StringSliceVarP(&Maintainers, "maintainers", "", []string{}, "GitHub users or groups that are considered maintainers") rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "Enable debug logging") @@ -117,7 +123,7 @@ func run(logger *zap.SugaredLogger) error { Maintainers: Maintainers, } - prHandler := &handlers.PullRequestHandler{ + prHandler := &handlers.PullRequestEventHandler{ ClientCreator: cc, Logger: logger, RequiredLabels: RequiredLabels, @@ -125,11 +131,20 @@ func run(logger *zap.SugaredLogger) error { Maintainers: Maintainers, } + prCreateHandler := &handlers.PullRequestCreateHandler{ + ClientCreator: cc, + Logger: logger, + TaxonomyRepo: TaxonomyRepo, + GithubUsername: GithubUsername, + GithubToken: GithubToken, + } + webhookHandler := githubapp.NewDefaultEventDispatcher(ghConfig, prCommentHandler, prHandler) http.Handle(githubapp.DefaultWebhookRoute, webhookHandler) - addr := net.JoinHostPort(HTTPAddress, strconv.Itoa(HTTPPort)) + //addr := net.JoinHostPort(HTTPAddress, strconv.Itoa(HTTPPort)) + addr := net.JoinHostPort("", strconv.Itoa(HTTPPort)) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) defer cancel() @@ -160,6 +175,9 @@ func run(logger *zap.SugaredLogger) error { } wg.Add(1) httpServer := &http.Server{Addr: addr} + http.HandleFunc("/pr/skill", prCreateHandler.SkillPRHandler) + http.HandleFunc("/pr/knowledge", prCreateHandler.KnowledgePRHandler) + go func() { logger.Infof("Starting server on %s...", addr) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/gobot/go.mod b/gobot/go.mod index 65bce353..3e8a07c6 100644 --- a/gobot/go.mod +++ b/gobot/go.mod @@ -5,37 +5,57 @@ go 1.21 require ( github.com/chmouel/gosmee v0.21.0 github.com/go-redis/redis/v8 v8.11.5 - github.com/google/go-github/v60 v60.0.0 + github.com/google/go-github/v61 v61.0.0 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/palantir/go-githubapp v0.25.0 github.com/pkg/errors v0.9.1 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 github.com/spf13/cobra v1.8.0 go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/google/go-github/v61 v61.0.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-github/v60 v60.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/tools v0.16.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect ) require ( + github.com/Pallinder/go-randomdata v1.2.0 github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/go-git/go-git/v5 v5.12.0 github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/go-github/v57 v57.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect diff --git a/gobot/go.sum b/gobot/go.sum index 1f067d9c..53570221 100644 --- a/gobot/go.sum +++ b/gobot/go.sum @@ -1,29 +1,64 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Pallinder/go-randomdata v1.2.0 h1:DZ41wBchNRb/0GfsePLiSwb0PHZmT67XY00lCDlaYPg= +github.com/Pallinder/go-randomdata v1.2.0/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg= github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chmouel/gosmee v0.21.0 h1:udMjRyW3NMTspnWwDezoYBqop0/IVNXlpCSI7r2dfl4= github.com/chmouel/gosmee v0.21.0/go.mod h1:9aGqzwBXARDUvuJQL5vu8denz3Ep9y7rgw1vBH+8WP4= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -43,8 +78,15 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -63,14 +105,16 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/palantir/go-githubapp v0.25.0 h1:BCeztIwWAr/0LBwop9jfcx0qpFGk74vcRiNWYigwxIA= github.com/palantir/go-githubapp v0.25.0/go.mod h1:LhYxlLwQJv7rIJd0KFXpRGKNPpP6ui6qU0TDeed95d4= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -80,8 +124,8 @@ github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -91,10 +135,15 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shurcooL/githubv4 v0.0.0-20240429030203-be2daab69064 h1:RCQBSFx5JrsbHltqTtJ+kN3U0Y3a/N/GlVdmRSoxzyE= github.com/shurcooL/githubv4 v0.0.0-20240429030203-be2daab69064/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -110,17 +159,23 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -128,35 +183,89 @@ go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/gobot/handlers/issue_comment.go b/gobot/handlers/issue_comment_event.go similarity index 100% rename from gobot/handlers/issue_comment.go rename to gobot/handlers/issue_comment_event.go diff --git a/gobot/handlers/pull_request_create.go b/gobot/handlers/pull_request_create.go new file mode 100644 index 00000000..a518701c --- /dev/null +++ b/gobot/handlers/pull_request_create.go @@ -0,0 +1,483 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/Pallinder/go-randomdata" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + githttp "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/google/go-github/v61/github" + "github.com/palantir/go-githubapp/githubapp" + "go.uber.org/zap" +) + +type PullRequestCreateHandler struct { + githubapp.ClientCreator + Logger *zap.SugaredLogger + TaxonomyRepo string + GithubUsername string + GithubToken string +} + +type PullRequestTask struct { + PullRequestCreateHandler + branchName string +} + +type SkillPRRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Task_description string `json:"task_description"` + Task_details string `json:"task_details"` + Title_work string `json:"title_work"` + Link_work string `json:"link_work"` + License_work string `json:"license_work"` + Creators string `json:"creators"` + Questions []string `json:"questions"` + Contexts []string `json:"contexts"` + Answers []string `json:"answers"` +} + +type KnowledgePRRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Task_description string `json:"task_description"` + Task_details string `json:"task_details"` + Repo string `json:"repo"` + Commit string `json:"commit"` + Patterns string `json:"patterns"` + Title_work string `json:"title_work"` + Link_work string `json:"link_work"` + Revision string `json:"revision"` + License_work string `json:"license_work"` + Creators string `json:"creators"` + Domain string `json:"domain"` + Questions []string `json:"questions"` + Answers []string `json:"answers"` +} + +const ( + TaxonomyPath = "taxonomy" + RepoSkillPath = "/compositional_skills/bot_skills" + RepoKnowledgePath = "/knowledge/bot_knowledge" + SkillStr = "skill" + KnowledgeStr = "knowledge" + YamlFileName = "qna.yaml" + AttributionFileName = "attribution.txt" +) + +func (prc *PullRequestCreateHandler) SkillPRHandler(w http.ResponseWriter, r *http.Request) { + var requestData SkillPRRequest + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + prc.Logger.Errorf("Error decoding skill request body %v", err) + http.Error(w, "Error decoding skill request body", http.StatusBadRequest) + return + } + + prc.Logger.Infof("Received Skill pull request data: %+v\n", requestData) + + prTask := PullRequestTask{ + PullRequestCreateHandler: *prc, + } + + prTask.branchName = prc.generateBranchName() + + // Clone the taxonomy repo + repo, err := prTask.cloneTaxonomyRepo() + if err != nil { + prc.Logger.Errorf("Error cloning taxonomy repo %v ", err) + http.Error(w, "Error cloning taxonomy repo", http.StatusInternalServerError) + return + } + + // Checkout branch for the PR + wt, err := prTask.checkoutBranch(repo) + if err != nil { + prc.Logger.Errorf("Error checking out branch %v ", err) + http.Error(w, "Error checking out branch", http.StatusInternalServerError) + return + } + + // Create commit for the PR + commitSha, err := prTask.createSkillCommit(wt, requestData) + if err != nil { + prc.Logger.Errorf("Error creating skill commit %v ", err) + http.Error(w, "Error creating skill commit", http.StatusInternalServerError) + return + } + + // Push the commit to the taxonomy repo + err = prTask.pushCommit(repo) + if err != nil { + prc.Logger.Errorf("Error pushing skill commit %v ", err) + http.Error(w, "Error pushing skill commit", http.StatusInternalServerError) + return + } + + // Create the pull request of the pushed branch to taxonomy main branch + pr, err := prTask.createPullRequest(SkillStr, requestData.Task_description, requestData.Task_details) + if err != nil { + prc.Logger.Errorf("Error creating skill pull request %v ", err) + http.Error(w, "Error creating skill pull request", http.StatusInternalServerError) + return + } + + prc.Logger.Infof("Pull request (%s) created successfully for skill with commit sha: %s", pr.GetHTMLURL(), commitSha) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Pull request (%s) created successfully for the skill", pr.GetHTMLURL()) + + //Delete the directory after the pull request is created + err = os.RemoveAll(prTask.branchName) + if err != nil { + prc.Logger.Errorf("Error deleting branch directory %v ", err) + return + } +} + +func (prc *PullRequestCreateHandler) KnowledgePRHandler(w http.ResponseWriter, r *http.Request) { + var requestData KnowledgePRRequest + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + prc.Logger.Errorf("Error decoding knowledge request body %v", err) + http.Error(w, "Error decoding knowledge request body", http.StatusBadRequest) + return + } + + prc.Logger.Infof("Received Knowledge pull request data: %+v\n", requestData) + + prTask := PullRequestTask{ + PullRequestCreateHandler: *prc, + } + + prTask.branchName = prc.generateBranchName() + + // Clone the taxonomy repo + repo, err := prTask.cloneTaxonomyRepo() + if err != nil { + prc.Logger.Errorf("Error cloning taxonomy repo %v ", err) + http.Error(w, "Error cloning taxonomy repo", http.StatusInternalServerError) + return + } + + // Checkout branch for the pull request + wt, err := prTask.checkoutBranch(repo) + if err != nil { + prc.Logger.Errorf("Error checking out branch %v ", err) + http.Error(w, "Error checking out branch", http.StatusInternalServerError) + return + } + + // Create commit for the pull request + commitSha, err := prTask.createKnowledgeCommit(wt, requestData) + if err != nil { + prc.Logger.Errorf("Error creating knowledge commit %v ", err) + http.Error(w, "Error creating knowledge commit", http.StatusInternalServerError) + return + } + + // Push the commit to the taxonomy repo + err = prTask.pushCommit(repo) + if err != nil { + prc.Logger.Errorf("Error pushing knowledge commit %v ", err) + http.Error(w, "Error pushing knowledge commit", http.StatusInternalServerError) + return + } + + // Create the pull request of the pushed branch to taxonomy main branch + pr, err := prTask.createPullRequest(KnowledgeStr, requestData.Task_description, requestData.Task_details) + if err != nil { + prc.Logger.Errorf("Error creating knowledge pull request %v ", err) + http.Error(w, "Error creating knowledge pull request", http.StatusInternalServerError) + return + } + + prc.Logger.Infof("Pull request (%s) created successfully for knowledge with commit sha: %s", pr.GetHTMLURL(), commitSha) + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Pull request (%s) created successfully for the knowledge", pr.GetHTMLURL()) + + //Delete the directory after the pull request is created + err = os.RemoveAll(prTask.branchName) + if err != nil { + prc.Logger.Errorf("Error deleting branch directory %v ", err) + return + } +} + +func (prt *PullRequestTask) cloneTaxonomyRepo() (*git.Repository, error) { + sugar := prt.Logger.With("user_name", prt.GithubUsername) + + workDir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("could not get working directory: %v", err) + } + taxonomyDir := path.Join(workDir, prt.branchName, TaxonomyPath) + + // Check if the taxonomy directory exists and delete if it does + if _, err := os.Stat(taxonomyDir); !os.IsNotExist(err) { + sugar.Warn("Taxonomy directory already exists, deleting") + err = os.RemoveAll(taxonomyDir) + if err != nil { + return nil, fmt.Errorf("could not delete taxonomy directory: %v", err) + } + } + + var r *git.Repository + if _, err := os.Stat(taxonomyDir); os.IsNotExist(err) { + sugar.Warnf("Taxonomy directory does not exist, cloning from %s", prt.TaxonomyRepo) + r, err = git.PlainClone(taxonomyDir, false, &git.CloneOptions{ + URL: prt.TaxonomyRepo, + Auth: &githttp.BasicAuth{ + Username: prt.GithubUsername, + Password: prt.GithubToken, + }, + Progress: os.Stdout, + }) + if err != nil { + return nil, fmt.Errorf("could not clone taxonomy git repo: %v", err) + } + } else { + r, err = git.PlainOpen(taxonomyDir) + if err != nil { + return nil, fmt.Errorf("could not open taxonomy git repo: %v", err) + } + } + return r, nil +} + +func (prt *PullRequestTask) checkoutBranch(repo *git.Repository) (*git.Worktree, error) { + // Create a new branch + headRef, err := repo.Head() + if err != nil { + return nil, fmt.Errorf("Error getting head reference: %v", err) + } + + branchRefName := plumbing.NewBranchReferenceName(prt.branchName) + + ref := plumbing.NewHashReference(branchRefName, headRef.Hash()) + + // The created reference is saved in the storage. + err = repo.Storer.SetReference(ref) + if err != nil { + return nil, fmt.Errorf("Error setting store reference: %v", err) + } + + // Add the file to the git repo + wt, err := repo.Worktree() + if err != nil { + return nil, fmt.Errorf("Error getting work tree: %v", err) + } + + // Checkout to the new branch + branchCoOpts := git.CheckoutOptions{ + Branch: plumbing.ReferenceName(branchRefName), + Force: true, + } + if err := wt.Checkout(&branchCoOpts); err != nil { + return nil, fmt.Errorf("Error creating new branch: %v", err) + } + + return wt, nil +} + +func (prt *PullRequestTask) createSkillCommit(wt *git.Worktree, requestData SkillPRRequest) (string, error) { + // Write the requestData to a file + workDir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("Error get current working directory: %v", err) + } + + dirPath := path.Join(workDir, prt.branchName, TaxonomyPath, RepoSkillPath, prt.branchName) + err = os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + return "", fmt.Errorf("Error creating directory: %v", err) + } + + filePath := path.Join(dirPath, YamlFileName) + yamlFile, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("Error creating skill yaml file: %v", err) + } + defer yamlFile.Close() + + // Convert requestData to yaml + yamlData, err := prt.generateSkillYaml(requestData) + if err != nil { + return "", fmt.Errorf("Error generating yaml from the skill pull request data: %v", err) + } + + _, err = yamlFile.WriteString(yamlData) + if err != nil { + return "", fmt.Errorf("Error writing to skill yaml file: %v", err) + } + + // Add the attribution file to the PR + attributionFilePath := path.Join(dirPath, AttributionFileName) + attributionFile, err := os.Create(attributionFilePath) + if err != nil { + return "", fmt.Errorf("Error creating attribution file for skill: %v", err) + } + defer attributionFile.Close() + + // Add the attribution file to the PR + attributionFileData := prt.generateSkillAttributionData(requestData) + + _, err = attributionFile.WriteString(attributionFileData) + if err != nil { + return "", fmt.Errorf("Error writing to skill attribution file: %v", err) + } + + skillDir := path.Join("."+RepoSkillPath, prt.branchName) + _, err = wt.Add(skillDir) + if err != nil { + return "", fmt.Errorf("Error adding skill files to git: %v", err) + } + + // Commit the changes with signature + signature := &object.Signature{ + Name: requestData.Name, + Email: requestData.Email, + When: time.Now(), + } + + commitMsg := fmt.Sprintf("%s: %s \n\n Signed-off-by: %s <%s>", SkillStr, requestData.Task_description, signature.Name, signature.Email) + + commit, err := wt.Commit(commitMsg, &git.CommitOptions{ + Author: signature, + Committer: signature, + }) + if err != nil { + return "", fmt.Errorf("Error committing skill related changes: %v", err) + } + return commit.String(), nil +} + +func (prt *PullRequestTask) createKnowledgeCommit(wt *git.Worktree, requestData KnowledgePRRequest) (string, error) { + // Write the requestData to a file + workDir, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("Error getting current working directory: %v", err) + } + + dirPath := path.Join(workDir, prt.branchName, TaxonomyPath, RepoKnowledgePath, prt.branchName) + err = os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + return "", fmt.Errorf("Error creating directory: %v", err) + } + + filePath := path.Join(dirPath, YamlFileName) + yamlFile, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("Error creating knowledge yaml file: %v", err) + } + defer yamlFile.Close() + + // Convert requestData to yaml + yamlData, err := prt.generateKnowledgeYaml(requestData) + if err != nil { + return "", fmt.Errorf("Error generating knowledge yaml from the pull request data: %v", err) + } + + _, err = yamlFile.WriteString(yamlData) + if err != nil { + return "", fmt.Errorf("Error writing to yaml file: %v", err) + } + + // Add the attribution file to the PR + attributionFilePath := path.Join(dirPath, AttributionFileName) + attributionFile, err := os.Create(attributionFilePath) + if err != nil { + return "", fmt.Errorf("Error creating knowledge attribution file: %v", err) + } + defer attributionFile.Close() + + // Convert requestData to yaml + attributionFileData := prt.generateKnowledgeAttributionData(requestData) + + _, err = attributionFile.WriteString(attributionFileData) + if err != nil { + return "", fmt.Errorf("Error writing to knowledge attribution file: %v", err) + } + + skillDir := path.Join("."+RepoKnowledgePath, prt.branchName) + _, err = wt.Add(skillDir) + if err != nil { + return "", fmt.Errorf("Error adding knowledge files to git: %v", err) + } + + // Commit the changes with signature + signature := &object.Signature{ + Name: requestData.Name, + Email: requestData.Email, + When: time.Now(), + } + + commitMsg := fmt.Sprintf("%s: %s \n\n Signed-off-by: %s <%s>", KnowledgeStr, requestData.Task_description, signature.Name, signature.Email) + + commit, err := wt.Commit(commitMsg, &git.CommitOptions{ + Author: signature, + Committer: signature, + }) + if err != nil { + return "", fmt.Errorf("Error committing knowledge related changes: %v", err) + } + + return commit.String(), nil +} + +func (prt *PullRequestTask) pushCommit(repo *git.Repository) error { + // Push the changes + err := repo.Push(&git.PushOptions{ + Auth: &githttp.BasicAuth{ + Username: prt.GithubUsername, + Password: prt.GithubToken, + }, + }) + if err != nil { + return fmt.Errorf("Error pushing changes: %v", err) + } + return nil +} + +func (prt *PullRequestTask) createPullRequest(prType string, prTitle string, prDescription string) (*github.PullRequest, error) { + // Create a PR + client, err := prt.ClientCreator.NewTokenClient(prt.GithubToken) + if err != nil { + return nil, fmt.Errorf("Error creating Github client: %v", err) + } + + ctx := context.Background() + + // Create a pull request + newPR := &github.NewPullRequest{ + Title: github.String(fmt.Sprintf("%s: %s", prType, prTitle)), + Head: github.String(prt.branchName), + Base: github.String("main"), + Body: github.String(prDescription), + MaintainerCanModify: github.Bool(true), + } + + pr, _, err := client.PullRequests.Create(ctx, prt.GithubUsername, TaxonomyPath, newPR) + if err != nil { + return nil, err + } + return pr, nil +} + +// TODO: We need better way to generate branch name +func (prc *PullRequestCreateHandler) generateBranchName() string { + str := randomdata.City() + str = strings.ReplaceAll(str, " ", "_") + return str +} diff --git a/gobot/handlers/pull_request.go b/gobot/handlers/pull_request_event.go similarity index 92% rename from gobot/handlers/pull_request.go rename to gobot/handlers/pull_request_event.go index 259c86d1..ce687cc8 100644 --- a/gobot/handlers/pull_request.go +++ b/gobot/handlers/pull_request_event.go @@ -12,7 +12,7 @@ import ( "go.uber.org/zap" ) -type PullRequestHandler struct { +type PullRequestEventHandler struct { githubapp.ClientCreator Logger *zap.SugaredLogger RequiredLabels []string @@ -20,11 +20,11 @@ type PullRequestHandler struct { Maintainers []string } -func (h *PullRequestHandler) Handles() []string { +func (h *PullRequestEventHandler) Handles() []string { return []string{"pull_request"} } -func (h *PullRequestHandler) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error { +func (h *PullRequestEventHandler) Handle(ctx context.Context, eventType, deliveryID string, payload []byte) error { var event github.PullRequestEvent if err := json.Unmarshal(payload, &event); err != nil { return errors.Wrap(err, "failed to parse issue comment event payload") diff --git a/gobot/handlers/yaml_util.go b/gobot/handlers/yaml_util.go new file mode 100644 index 00000000..67dc22aa --- /dev/null +++ b/gobot/handlers/yaml_util.go @@ -0,0 +1,139 @@ +package handlers + +import ( + "bytes" + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +type SkillYaml struct { + Task_description string `yaml:"task_description"` + Created_by string `yaml:"created_by"` + Seed_examples []struct { + Question yaml.Node + Context yaml.Node + Answer yaml.Node + } `yaml:"seed_examples"` +} + +type KnowledgeYaml struct { + Task_description string `yaml:"task_description"` + Created_by string `yaml:"created_by"` + Domain string `yaml:"domain"` + Seed_examples []struct { + Question yaml.Node + Answer yaml.Node + } `yaml:"seed_examples"` + Document struct { + Repo string `yaml:"repo"` + Commit string `yaml:"commit"` + Patterns []string `yaml:"patterns"` + } `yaml:"document"` +} + +func (prc *PullRequestCreateHandler) generateKnowledgeYaml(requestData KnowledgePRRequest) (string, error) { + knowledgeYaml := KnowledgeYaml{ + Task_description: strings.TrimSpace(requestData.Task_description), + Created_by: strings.TrimSpace(requestData.Name), + Domain: strings.TrimSpace(requestData.Domain), + Seed_examples: []struct { + Question yaml.Node + Answer yaml.Node + }{}, + Document: struct { + Repo string `yaml:"repo"` + Commit string `yaml:"commit"` + Patterns []string `yaml:"patterns"` + }{ + Repo: strings.TrimSpace(requestData.Repo), + Commit: strings.TrimSpace(requestData.Commit), + Patterns: strings.Split(strings.TrimSpace(requestData.Patterns), ","), + }, + } + + for i, question := range requestData.Questions { + knowledgeYaml.Seed_examples = append(knowledgeYaml.Seed_examples, struct { + Question yaml.Node + Answer yaml.Node + }{ + yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: strings.TrimSpace(question), + }, + yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: strings.TrimSpace(requestData.Answers[i]), + }, + }) + } + + // Generate the yaml file using new yaml encoder + var buf bytes.Buffer + yamlEncoder := yaml.NewEncoder(&buf) + err := yamlEncoder.Encode(knowledgeYaml) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func (prc *PullRequestCreateHandler) generateKnowledgeAttributionData(requestData KnowledgePRRequest) string { + return fmt.Sprintf("Title of work: %s \nLink to work: %s \nRevision: %s \nLicense of the work: %s \nCreator names: %s", + strings.TrimSpace(requestData.Title_work), strings.TrimSpace(requestData.Link_work), + strings.TrimSpace(requestData.Revision), strings.TrimSpace(requestData.License_work), strings.TrimSpace(requestData.Creators)) +} + +func (prc *PullRequestCreateHandler) generateSkillYaml(requestData SkillPRRequest) (string, error) { + skillYaml := SkillYaml{ + Task_description: strings.TrimSpace(requestData.Task_description), + Created_by: strings.TrimSpace(requestData.Name), + Seed_examples: []struct { + Question yaml.Node + Context yaml.Node + Answer yaml.Node + }{}, + } + + for i, question := range requestData.Questions { + skillYaml.Seed_examples = append(skillYaml.Seed_examples, struct { + Question yaml.Node + Context yaml.Node + Answer yaml.Node + }{ + yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: strings.TrimSpace(question), + }, + yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: strings.TrimSpace(requestData.Contexts[i]), + }, + yaml.Node{ + Kind: yaml.ScalarNode, + Style: yaml.FoldedStyle, + Value: strings.TrimSpace(requestData.Answers[i]), + }, + }) + } + + // Generate the yaml file using new yaml encoder + var buf bytes.Buffer + yamlEncoder := yaml.NewEncoder(&buf) + err := yamlEncoder.Encode(skillYaml) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func (prc *PullRequestCreateHandler) generateSkillAttributionData(requestData SkillPRRequest) string { + return fmt.Sprintf("Title of work: %s \nLink to work: %s \nLicense of the work: %s \nCreator names: %s", + strings.TrimSpace(requestData.Title_work), strings.TrimSpace(requestData.Link_work), + strings.TrimSpace(requestData.License_work), strings.TrimSpace(requestData.Creators)) +} diff --git a/ui/apiserver/apiserver.go b/ui/apiserver/apiserver.go index 1fa65804..2ea8d06b 100644 --- a/ui/apiserver/apiserver.go +++ b/ui/apiserver/apiserver.go @@ -26,6 +26,7 @@ const ( ) const PreCheckEndpointURL = "https://merlinite-7b-vllm-openai.apps.fmaas-backend.fmaas.res.ibm.com/v1" +const InstructLabBotUrl = "http://bot:8081" type ApiServer struct { router *gin.Engine @@ -34,6 +35,7 @@ type ApiServer struct { ctx context.Context testMode bool preCheckEndpointURL string + instructLabBotUrl string } type JobData struct { @@ -59,6 +61,38 @@ type ChatRequest struct { Context string `json:"context"` } +type SkillPRRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Task_description string `json:"task_description"` + Task_details string `json:"task_details"` + Title_work string `json:"title_work"` + Link_work string `json:"link_work"` + License_work string `json:"license_work"` + Creators string `json:"creators"` + Questions []string `json:"questions"` + Contexts []string `json:"contexts"` + Answers []string `json:"answers"` +} + +type KnowledgePRRequest struct { + Name string `json:"name"` + Email string `json:"email"` + Task_description string `json:"task_description"` + Task_details string `json:"task_details"` + Repo string `json:"repo"` + Commit string `json:"commit"` + Patterns string `json:"patterns"` + Title_work string `json:"title_work"` + Link_work string `json:"link_work"` + Revision string `json:"revision"` + License_work string `json:"license_work"` + Creators string `json:"creators"` + Domain string `json:"domain"` + Questions []string `json:"questions"` + Answers []string `json:"answers"` +} + func (api *ApiServer) chatHandler(c *gin.Context) { var req ChatRequest @@ -80,6 +114,117 @@ func (api *ApiServer) chatHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"answer": answer}) } +func (api *ApiServer) skillPRHandler(c *gin.Context) { + + var prData SkillPRRequest + if err := c.ShouldBindJSON(&prData); err != nil { + api.logger.Error("Failed to bind JSON: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + api.logger.Infof("Received Skill pull request data: %v", prData) + + prJson, err := json.Marshal(prData) + if err != nil { + api.logger.Errorf("Error encoding JSON: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + + url := fmt.Sprintf("%s/pr/skill", InstructLabBotUrl) + resp, err := api.sendPostRequest(url, bytes.NewBuffer(prJson)) + if err != nil { + api.logger.Errorf("Error sending post request to bot http server: %v -- %v", err, resp) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + defer resp.Body.Close() + + responseBody := new(bytes.Buffer) + _, err = responseBody.ReadFrom(resp.Body) + if err != nil { + api.logger.Errorf("Error reading response body: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + } + + if resp.StatusCode != http.StatusOK { + api.logger.Errorf("Error response (code : %s) from bot http server: %v", resp.StatusCode, responseBody.String()) + c.JSON(http.StatusInternalServerError, gin.H{"error": responseBody.String()}) + return + } + + api.logger.Infof("Skill pull request response: %v", responseBody.String()) + c.JSON(http.StatusOK, gin.H{"msg": responseBody.String()}) +} + +func (api *ApiServer) knowledgePRHandler(c *gin.Context) { + + var prData KnowledgePRRequest + if err := c.ShouldBindJSON(&prData); err != nil { + api.logger.Error("Failed to bind JSON:", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) + return + } + + api.logger.Infof("Received Knowledge pull request data: %v", prData) + + prJson, err := json.Marshal(prData) + if err != nil { + api.logger.Errorf("Error encoding JSON: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + url := fmt.Sprintf("%s/pr/knowledge", InstructLabBotUrl) + resp, err := api.sendPostRequest(url, bytes.NewBuffer(prJson)) + if err != nil { + api.logger.Errorf("Error sending post request to bot http server: %v -- %v", err, resp) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + return + } + + responseBody := new(bytes.Buffer) + _, err = responseBody.ReadFrom(resp.Body) + if err != nil { + api.logger.Errorf("Error reading response body: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err}) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + api.logger.Errorf("Error response (code : %s) from bot http server: %v", resp.StatusCode, responseBody.String()) + c.JSON(http.StatusInternalServerError, gin.H{"error": responseBody.String()}) + return + } + + api.logger.Infof("Knowledge pull request response: %v", responseBody.String()) + + c.JSON(http.StatusOK, gin.H{"msg": responseBody.String()}) +} + +// Sent http post request using custom client with zero timeout +func (api *ApiServer) sendPostRequest(url string, body io.Reader) (*http.Response, error) { + client := &http.Client{ + Timeout: 0 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + request, err := http.NewRequest("POST", url, body) + if err != nil { + api.logger.Errorf("Error creating http request: %v", err) + return nil, err + } + request.Header.Set("Content-Type", "application/json") + response, err := client.Do(request) + if err != nil { + api.logger.Errorf("Error sending http request: %v", err) + return nil, err + } + return response, nil +} + func (api *ApiServer) getAllJobs(c *gin.Context) { resultsJobIDs, err := api.redis.LRange(context.Background(), redisQueueGenerate, 0, -1).Result() if err != nil { @@ -160,6 +305,8 @@ func (api *ApiServer) setupRoutes(apiUser, apiPass string) { authorized.Use(AuthRequired(apiUser, apiPass)) authorized.GET("/jobs", api.getAllJobs) authorized.POST("/chat", api.chatHandler) + authorized.POST("/pr/skill", api.skillPRHandler) + authorized.POST("/pr/knowledge", api.knowledgePRHandler) api.router.GET("/", func(c *gin.Context) { c.String(http.StatusOK, "IL Redis Queue") @@ -233,7 +380,7 @@ func setupLogger(debugMode bool) *zap.SugaredLogger { return logger.Sugar() } -// fetchModelName hits the defined precheckEndpoint with "/models" appended to extract the model name. +// fetchModelName hits the defined precheck endpoint with "/models" appended to extract the model name. // If fullName is true, it returns the entire ID value; if false, it returns the parsed out name after the double hyphens. func (api *ApiServer) fetchModelName(fullName bool) (string, error) { // Ensure the endpoint URL ends with "/models" @@ -312,7 +459,8 @@ func main() { redisAddress := pflag.String("redis-server", "localhost:6379", "Redis server address") apiUser := pflag.String("api-user", "", "API username") apiPass := pflag.String("api-pass", "", "API password") - preCheckEndpointURL := pflag.String("precheck-endpoint", PreCheckEndpointURL, "PreCheck endpoint URL") + preCheckEndpointURL := pflag.String("precheck-endpoint", PreCheckEndpointURL, "Precheck endpoint URL") + InstructLabBotUrl := pflag.String("bot-url", InstructLabBotUrl, "InstructLab Bot URL") pflag.Parse() logger := setupLogger(*debugFlag) @@ -334,6 +482,7 @@ func main() { ctx: context.Background(), testMode: *testMode, preCheckEndpointURL: *preCheckEndpointURL, + instructLabBotUrl: *InstructLabBotUrl, } svr.setupRoutes(*apiUser, *apiPass) diff --git a/ui/package-lock.json b/ui/package-lock.json index a84d6f7d..9b25a2f0 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", + "@types/js-yaml": "^4.0.9", "@types/react-router-dom": "^5.3.3", "@types/victory": "^33.1.5", "@typescript-eslint/eslint-plugin": "^7.7.1", @@ -2188,6 +2189,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, "node_modules/@types/jsdom": { "version": "20.0.1", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", diff --git a/ui/package.json b/ui/package.json index 9b4c8e5d..5bd6e787 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.4", + "@types/js-yaml": "^4.0.9", "@types/react-router-dom": "^5.3.3", "@types/victory": "^33.1.5", "@typescript-eslint/eslint-plugin": "^7.7.1", diff --git a/ui/src/app/Contribute/Knowledge/Knowledge.css b/ui/src/app/Contribute/Knowledge/Knowledge.css new file mode 100644 index 00000000..35e481f5 --- /dev/null +++ b/ui/src/app/Contribute/Knowledge/Knowledge.css @@ -0,0 +1,83 @@ +/* Knowledge CSS */ + +.main-k { + display: block; + /* Use flexbox */ +} + +.dataarea-k { + display: flex; + /* Use flexbox */ + width: 100%; + height: 80%; + margin-top: 10px; + margin-left: 10px; + margin-right: 10px; +} + +.knowledge-k { + width: 60%; + height: 100%; + margin-top: 10px; + margin-left: 10px; + margin-right: 10px; + scroll-behavior: smooth; + overflow: scroll; +} + +.metadata-k { + width: 35%; + height: 60%; + margin-top: 60px; + margin-left: 10px; + margin-right: 10px; +} + +.info-k { + width: 100%; + height: 40%; + margin-top: 50px; + +} + +.attribution-k { + width: 100%; + height: 30%; + margin-top: 100px; + margin-right: 10px; +} + +.document-k { + width: 100%; + height: 30%; + margin-right: 10px; +} + +.submit-k { + display: inline-block; + margin-top: 30px; + margin-left: 50%; + +} + +.submit-k:hover { + background-color: #45a049; + /* Darker Green */ +} + +.title-k { + align-items: center; + margin-bottom: 10px; + text-align: center; + text-shadow: #45a049; + font-size: large; + font-weight: bold; + color: rgb(4, 4, 135); +} + +.heading-k { + text-align: left; + text-shadow: #45a049; + font-size: large; + color: rgb(4, 4, 135); +} \ No newline at end of file diff --git a/ui/src/app/Contribute/Knowledge/Knowledge.tsx b/ui/src/app/Contribute/Knowledge/Knowledge.tsx new file mode 100644 index 00000000..1fa72651 --- /dev/null +++ b/ui/src/app/Contribute/Knowledge/Knowledge.tsx @@ -0,0 +1,372 @@ +import React, { useState } from 'react'; +import './Knowledge.css'; +import { usePostKnowledgePR } from "@app/common/HooksPostKnowledgePR"; +import { + Alert, + AlertActionCloseButton, + ActionGroup, + Button, + Text, + TextInput, + Form, + FormGroup, + TextArea, +} from '@patternfly/react-core'; + +export const KnowledgeForm: React.FunctionComponent = () => { + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [task_description, setTaskDescription] = useState(''); + const [task_details, setTaskDetails] = useState(''); + const [domain, setDomain] = useState(''); + + const [repo, setRepo] = useState(''); + const [commit, setCommit] = useState(''); + const [patterns, setPatterns] = useState(''); + + const [title_work, setTitleWork] = useState(''); + const [link_work, setLinkWork] = useState(''); + const [revision, setRevision] = useState(''); + const [license_work, setLicenseWork] = useState(''); + const [creators, setCreators] = useState(''); + + const [questions, setQuestions] = useState(new Array(5).fill('')); + const [answers, setAnswers] = useState(new Array(5).fill('')); + const [isSuccessAlertVisible, setIsSuccessAlertVisible] = useState(false); + const [isFailureAlertVisible, setIsFailureAlertVisible] = useState(false); + + const [failure_alert_title, setFailureAlertTitle] = useState(''); + const [failure_alert_message, setFailureAlertMessage] = useState(''); + + const [success_alert_title, setSuccessAlertTitle] = useState(''); + const [success_alert_message, setSuccessAlertMessage] = useState(''); + + + const { postKnowledgePR } = usePostKnowledgePR(); + + const handleInputChange = (index: number, type: string, value: string) => { + switch (type) { + case 'question': + setQuestions((prevQuestions) => { + const updatedQuestions = [...prevQuestions]; + updatedQuestions[index] = value; + return updatedQuestions; + }); + break; + case 'answer': + setAnswers((prevAnswers) => { + const updatedAnswers = [...prevAnswers]; + updatedAnswers[index] = value; + return updatedAnswers; + }); + break; + default: + break; + } + }; + + const resetForm = () => { + setEmail(''); + setName(''); + + setTaskDescription(''); + setTaskDetails(''); + setDomain(''); + setQuestions(new Array(5).fill('')); + setAnswers(new Array(5).fill('')); + + setRepo(''); + setCommit(''); + setPatterns(''); + + setTitleWork(''); + setLinkWork(''); + setLicenseWork(''); + setCreators(''); + setRevision(''); + }; + + const onCloseSuccessAlert = () => { + setSuccessAlertTitle('Knowledge contribution submitted successfully!'); + setSuccessAlertMessage('Thank you for your contribution!!'); + setIsSuccessAlertVisible(false); + }; + + const onCloseFailureAlert = () => { + setFailureAlertTitle('Failed to submit your Knowledge contribution!'); + setFailureAlertMessage('Please try again later.'); + setIsFailureAlertVisible(false); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + // Make sure all questions and answers are filled + if (questions.some((question) => question === '') || answers.some((answer) => answer === '')) { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please make sure all the questions and answers are filled!'); + setIsFailureAlertVisible(true); + return; + } + + // Make sure all the info fields are filled + if ( + email === '' || + name === '' || + task_description === '' || + task_details === '' || + domain === '' || + repo === '' || + commit === '' || + patterns === '') { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please make sure all the Info fields are filled!'); + setIsFailureAlertVisible(true); + return; + } + + // Make sure email has a valid format + const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; + if (!emailRegex.test(email)) { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please enter a valid email address!'); + setIsFailureAlertVisible(true); + return; + } + + // Make sure all the attribution fields are filled + if ( + title_work === '' || + link_work === '' || + revision === '' || + license_work === '' || + creators === '' + ) { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please make sure all the Attribution fields are filled!'); + setIsFailureAlertVisible(true); + return; + } + + // Make sure all the questions are unique + const uniqueQuestions = new Set(questions); + if (uniqueQuestions.size !== questions.length) { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please make sure all the questions are unique!'); + setIsFailureAlertVisible(true); + return; + } + + const uniqueAnswer = new Set(answers); + if (uniqueAnswer.size !== answers.length) { + setFailureAlertTitle('Something went wrong!'); + setFailureAlertMessage('Please make sure all the answers are unique!'); + setIsFailureAlertVisible(true); + return; + } + + const [res, err] = await postKnowledgePR({ + name: name, + email: email, + task_description: task_description, + task_details: task_details, + repo: repo, + commit: commit, + patterns: patterns, + title_work: title_work, + link_work: link_work, + revision: revision, + license_work: license_work, + creators: creators, + domain: domain, + questions, + answers, + }); + + if (err !== null) { + setFailureAlertTitle('Failed to submit your Knowledge contribution!'); + setFailureAlertMessage(err); + setIsFailureAlertVisible(true); + return; + } + + if (res !== null) { + setSuccessAlertTitle('Knowledge contribution submitted successfully!'); + setSuccessAlertMessage(res) + setIsSuccessAlertVisible(true); + resetForm(); + } + console.log('Knowledge submitted successfully : ' + res); + }; + + return ( +
+ {isSuccessAlertVisible && ( + } + > + {success_alert_message} + + )} + {isFailureAlertVisible && ( + } + > + {failure_alert_message} + + )} +
+
+
+ Contribute a Knowledge + {[...Array(5)].map((_, index) => ( + + Example : {index + 1} +