renuo-cli 4.30.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e403749b46f9d289d45f218209831c68a8c624840f4df20e1315127d4ba2bbac
4
- data.tar.gz: 85cd1b725f516ab96fa5da67355c9268d8397eeb3b7db4017263b3583558f4d8
3
+ metadata.gz: 8462a65e3075cb5e8a3f7c01a254ac225e81ce774ab53d16a1915883e6d4969a
4
+ data.tar.gz: 4985892734d7db16156a8139c325779c4d60f8763de9b7d690cac7b91503ce77
5
5
  SHA512:
6
- metadata.gz: 193344bdebb9952145a2c8e948e04b8ad85f8d04d0d642c8c0f00cd1173639454c7306a7c80665de5bcc68c7e78a5de74f4406012de7e4d61cda5d0a9a173211
7
- data.tar.gz: c6bd00ecd6bedcde704ab346dcfa3cdd0affae38a0413c457636c575c561b12432a9a06c1422de2673b0ff0a40bb8c7e03f66626321d7c756eb0e8ca5927ae0a
6
+ metadata.gz: 1dfd2825a7348c1ca48eb9a337aa5bb2d2c217c36e2653914fec2a3db8cc8c0a034aebe486142809895f75e7c0442fd41e78224173b4bd82460e66550bdc798b
7
+ data.tar.gz: eb01997fbb165adaaf5a904306b379ae1875098d16e65d5b3b62bb1396527c71680aa69ddc604cfcf43270e12ce83f46786c7e638e8429fb3026ea6d80f21cd1
@@ -33,6 +33,7 @@ class Renuo::Cli::Commands::ConfigureSemaphore
33
33
 
34
34
  create_semaphore_notification
35
35
  create_semaphore_secrets
36
+ create_nctl_service_account_secret
36
37
  create_semaphore_deployment_targets
37
38
  end
38
39
 
@@ -52,6 +53,42 @@ class Renuo::Cli::Commands::ConfigureSemaphore
52
53
  system("sem create secret -p #{project_name} #{project_name}")
53
54
  end
54
55
 
56
+ DEPLOIO_ORGANIZATION = "renuo"
57
+
58
+ def deploio_project
59
+ "#{DEPLOIO_ORGANIZATION}-#{project_name}"
60
+ end
61
+
62
+ def service_account_name
63
+ "#{project_name}-semaphore"
64
+ end
65
+
66
+ def nctl_secret_name
67
+ "nctl-#{project_name}"
68
+ end
69
+
70
+ def create_nctl_service_account_secret
71
+ if system(nctl_create_service_account_command)
72
+ system(sem_create_nctl_secret_command)
73
+ else
74
+ warn("Could not create the API service account. Does the Deploio project #{deploio_project} exist? " \
75
+ "Create it with `renuo create-deploio-app`, then run:\n\n " \
76
+ "#{nctl_create_service_account_command}\n #{sem_create_nctl_secret_command}")
77
+ end
78
+ end
79
+
80
+ def nctl_create_service_account_command
81
+ "nctl create asa #{service_account_name} --project #{deploio_project} --wait"
82
+ end
83
+
84
+ def sem_create_nctl_secret_command
85
+ get_asa = "nctl get asa #{service_account_name} --project #{deploio_project}"
86
+ "sem create secret #{nctl_secret_name} -p #{project_name} " \
87
+ "-e NCTL_API_CLIENT_ID=\"$(#{get_asa} --print-client-id)\" " \
88
+ "-e NCTL_API_CLIENT_SECRET=\"$(#{get_asa} --print-client-secret)\" " \
89
+ "-e NCTL_ORGANIZATION=#{DEPLOIO_ORGANIZATION}"
90
+ end
91
+
55
92
  def create_semaphore_deployment_targets
56
93
  %w[main develop].each do |branch|
57
94
  system("sem create dt #{branch} -p #{project_name} --object-rule branch,exact,#{branch}")
@@ -175,7 +175,7 @@ class Renuo::Cli::Commands::CreateDeploioApp # rubocop:disable Metrics/ClassLeng
175
175
  --basic-auth=#{environment == "develop"} \\
176
176
  --build-env=SECRET_KEY_BASE="$(rails secret)" \\
177
177
  --language=ruby \\
178
- --buildpack-stack=heroku \\
178
+ --buildpack-type=heroku \\
179
179
  --size=mini \\
180
180
  --replicas=1
181
181
  OUTPUT
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../helpers/review_app_notification"
4
+
5
+ module Renuo::Cli::Commands::Notify
6
+ module Slack
7
+ class ReviewAppDeploy
8
+ include ReviewAppNotification
9
+ include Renuo::Cli::Services
10
+
11
+ command "notify:slack:review-app-deploy" do |c|
12
+ c.syntax = "renuo notify:slack:review-app-deploy [options]"
13
+ c.summary = "Posts the branch's review app URL (with its basic auth credentials) and pull request to Slack."
14
+ c.description = "#{c.summary} The Slack incoming webhook is read from SLACK_WEBHOOK_URL, the GitHub " \
15
+ "repository from SEMAPHORE_GIT_REPO_SLUG and bundler's GitHub credentials (a token or " \
16
+ "user:token) from BUNDLE_GITHUB__COM to link the branch's open pull request. On Semaphore, " \
17
+ "it links the workflow (SEMAPHORE_ORGANIZATION_URL, SEMAPHORE_WORKFLOW_ID) offering the " \
18
+ "review app promotions."
19
+ ReviewAppNotification.define_options(c)
20
+ c.example "renuo notify:slack:review-app-deploy -p renuo-acme -b feature/42-migrate-to-deploio " \
21
+ "--channel '#project-acme-notifications'",
22
+ "Announces the branch's review app in the project's Slack channel"
23
+ c.action { |_args, options| new(options).run }
24
+ end
25
+
26
+ COLOR = "#141d50"
27
+
28
+ private
29
+
30
+ def message_lines
31
+ credentials = basic_auth_credentials
32
+ pull_request = fetch_pull_request
33
+
34
+ [
35
+ "Review app is ready: #{app_link(credentials)}",
36
+ pull_request && pull_request_line(pull_request),
37
+ branch_line
38
+ ].compact
39
+ end
40
+
41
+ def pull_request_line(pull_request)
42
+ "Pull request: <#{pull_request["html_url"]}|##{pull_request["number"]} #{pull_request["title"]}>"
43
+ end
44
+
45
+ def app_link(credentials)
46
+ url = app_url
47
+ url.userinfo = userinfo(credentials) if credentials
48
+ "<#{url}|#{url.host}>"
49
+ end
50
+
51
+ def userinfo(credentials)
52
+ credentials.values_at("username", "password").map { |value| URI.encode_www_form_component(value) }.join(":")
53
+ end
54
+
55
+ def app_url
56
+ url = Deploio.fetch_resource("app", @app_name, @project).to_h.dig("status", "atProvider", "defaultURLs", 0)
57
+ URI(url || abort("#{@app_name} has no URL assigned yet"))
58
+ end
59
+
60
+ # Deploio assigns basic auth credentials per app, so the review app's differ from the origin's.
61
+ def basic_auth_credentials
62
+ stdout, = system!("nctl get app #{@app_name} -p #{@project} -o json --basic-auth-credentials", quiet: true)
63
+ Array(JSON.parse(stdout)).first&.dig("basicauth")
64
+ end
65
+
66
+ def fetch_pull_request
67
+ credentials = ENV.fetch("BUNDLE_GITHUB__COM", "")
68
+ return if credentials.empty?
69
+
70
+ response = Net::HTTP.start("api.github.com", 443, use_ssl: true) do |http|
71
+ http.request(pull_request_request(credentials))
72
+ end
73
+ return JSON.parse(response.body).first if response.is_a?(Net::HTTPSuccess)
74
+
75
+ warn "GitHub pull request lookup failed: #{response.code}"
76
+ nil
77
+ end
78
+
79
+ def pull_request_request(credentials)
80
+ query = URI.encode_www_form(state: "open", head: "#{@repo.split("/").first}:#{@branch}")
81
+ request = Net::HTTP::Get.new("/repos/#{@repo}/pulls?#{query}", "Accept" => "application/vnd.github+json")
82
+ user, token = credentials.split(":", 2)
83
+ request.basic_auth(user, token || "x-oauth-basic")
84
+ request
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../helpers/review_app_notification"
4
+
5
+ module Renuo::Cli::Commands::Notify
6
+ module Slack
7
+ class ReviewAppPurge
8
+ include ReviewAppNotification
9
+
10
+ command "notify:slack:review-app-purge" do |c|
11
+ c.syntax = "renuo notify:slack:review-app-purge [options]"
12
+ c.summary = "Announces on Slack that the branch's review app was purged."
13
+ c.description = "#{c.summary} The Slack incoming webhook is read from SLACK_WEBHOOK_URL and the GitHub " \
14
+ "repository from SEMAPHORE_GIT_REPO_SLUG. On Semaphore, it links the workflow " \
15
+ "(SEMAPHORE_ORGANIZATION_URL, SEMAPHORE_WORKFLOW_ID) offering the review app promotions."
16
+ ReviewAppNotification.define_options(c)
17
+ c.example "renuo notify:slack:review-app-purge -p renuo-acme -b feature/42-migrate-to-deploio " \
18
+ "--channel '#project-acme-notifications'",
19
+ "Announces that the branch's review app was purged"
20
+ c.action { |_args, options| new(options).run }
21
+ end
22
+
23
+ COLOR = "#9e9e9e"
24
+
25
+ private
26
+
27
+ def message_lines
28
+ ["Review app #{@app_name} was purged", branch_line]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -13,8 +13,9 @@ These are the steps of command execution in the happy path:
13
13
  1. Create or update `renuo review-app:deploy`
14
14
  1. _Intentional gap for configuration. Consider this the wiggle-room for LLMs_
15
15
  1. Start the paused app `renuo review-app:start`
16
- 1. Announce it on Slack `renuo review-app:notify`
16
+ 1. Announce it on Slack `renuo notify:slack:review-app-deploy`
17
17
  1. Cleanup `renuo review-app:purge`
18
+ 1. Announce the cleanup on Slack `renuo notify:slack:review-app-purge`
18
19
 
19
20
  ## Design Choices
20
21
 
@@ -28,6 +29,9 @@ These are the steps of command execution in the happy path:
28
29
  * All `nctl` commands are routed through `system!` and are logged in a command trail for auditing.
29
30
  * Configuration of system tools like `psql` is intentionally out of scope.
30
31
  But CI templating can give you some pointers.
31
- * `review-app:notify` posts the review app URL (carrying its Deploio-assigned basic auth credentials) and the
32
- branch's open pull request to a Slack incoming webhook (`SLACK_WEBHOOK_URL`). The pull request lookup uses
32
+ * `notify:slack:review-app-deploy` posts the review app URL (carrying its Deploio-assigned basic auth credentials) and
33
+ the branch's open pull request to a Slack incoming webhook (`SLACK_WEBHOOK_URL`). The pull request lookup uses
33
34
  bundler's `BUNDLE_GITHUB__COM` (a token or `user:token`) and never fails the pipeline when GitHub refuses it.
35
+ Incoming webhooks can't edit messages, so `notify:slack:review-app-purge` posts a separate one once purged.
36
+ On Semaphore, both messages link the branch's workflow offering the promotions. It's a link, not a button:
37
+ buttons posted by an incoming webhook show a warning unless they come from an interactive Slack app.
@@ -16,7 +16,7 @@ module Renuo::Cli::Commands::ReviewApp
16
16
  c.description = c.summary
17
17
  c.option "-p", "--project <name>", String, "The name of the Deploio project"
18
18
  c.option "--origin-pattern <pattern>", String, "Name pattern identifying the origin app to copy from"
19
- c.option "--slack-channel <name>", String, "The Slack channel notified once a review app is ready"
19
+ c.option "--slack-channel <name>", String, "The Slack channel notified when a review app is ready or purged"
20
20
  c.action { |_args, options| new(options).run }
21
21
  end
22
22
 
@@ -25,7 +25,8 @@ module Renuo::Cli::Commands::ReviewApp
25
25
  write_or_warn DEPLOY_PIPELINE_TARGET_FILE,
26
26
  render(DEPLOY_PIPELINE_TEMPLATE_FILE,
27
27
  { project:, origin_pattern:, secret_name:, slack_channel: })
28
- write_or_warn PURGE_PIPELINE_TARGET_FILE, render(PURGE_PIPELINE_TEMPLATE_FILE, { project:, secret_name: })
28
+ write_or_warn PURGE_PIPELINE_TARGET_FILE,
29
+ render(PURGE_PIPELINE_TEMPLATE_FILE, { project:, secret_name:, slack_channel: })
29
30
  print_instructions
30
31
  end
31
32
 
@@ -75,7 +76,8 @@ module Renuo::Cli::Commands::ReviewApp
75
76
  pipeline_file: review-app-purge.yml
76
77
 
77
78
  Both buttons appear on every branch workflow in Semaphore. Deploy and purge the review app
78
- of a branch by clicking the promotion on that branch's workflow.
79
+ of a branch by clicking the promotion on that branch's workflow. The Slack message announcing
80
+ a review app links that workflow, so it can be purged from there.
79
81
  INSTRUCTIONS
80
82
  end
81
83
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "review_app_helper"
4
+ require_relative "safe_review_app_name_contract"
5
+ require_relative "../services/review_app_slack_webhook"
6
+
7
+ module ReviewAppNotification
8
+ include ReviewAppHelper
9
+ include Contracts::Core
10
+
11
+ def self.define_options(command)
12
+ command.option "-p", "--project <name>", String, "The name of the Deploio project"
13
+ command.option "-b", "--branch <name>", String, "The Git branch"
14
+ command.option "--channel <name>", String, "The Slack channel to post to"
15
+ command.option "--repo <owner/name>", String, "The GitHub repository, defaults to SEMAPHORE_GIT_REPO_SLUG"
16
+ end
17
+
18
+ def run
19
+ @slack.post([*message_lines, workflow_line].compact.join("\n"), color: self.class::COLOR)
20
+ CmdTrail::Print.call
21
+ puts "\nnotified #{@channel}"
22
+ end
23
+
24
+ private
25
+
26
+ def initialize(options)
27
+ @project = options.project || abort("missing project name")
28
+ @branch = options.branch || abort("missing branch name")
29
+ @channel = options.channel || abort("missing Slack channel")
30
+ @repo = options.repo || ENV.fetch("SEMAPHORE_GIT_REPO_SLUG", nil).presence ||
31
+ abort("missing GitHub repository, set SEMAPHORE_GIT_REPO_SLUG or pass --repo")
32
+ @slack = Renuo::Cli::Services::ReviewAppSlackWebhook.new(@channel)
33
+ derive_app_name(@branch)
34
+ end
35
+
36
+ Contract String => SafeReviewAppNameContract
37
+ def derive_app_name(branch)
38
+ @app_name = review_app_name(branch)
39
+ end
40
+
41
+ def branch_line
42
+ "Branch: <https://github.com/#{@repo}/tree/#{@branch}|#{@branch}>"
43
+ end
44
+
45
+ # The deploy and purge promotions both run within the branch's workflow, which offers the other one.
46
+ # A link, because a button posted by an incoming webhook shows a warning without an interactive Slack app.
47
+ def workflow_line
48
+ organization_url, workflow_id = ENV.values_at("SEMAPHORE_ORGANIZATION_URL", "SEMAPHORE_WORKFLOW_ID")
49
+ return if organization_url.blank? || workflow_id.blank?
50
+
51
+ "Workflow: <#{organization_url}/workflows/#{workflow_id}|Open on Semaphore>"
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renuo::Cli::Services::ReviewAppSlackWebhook
4
+ SENDER = { username: "Deploio Review Apps", icon_url: "https://deploio.ch/meta/apple-touch-icon.png" }.freeze
5
+
6
+ def initialize(channel)
7
+ @channel = channel
8
+ @url = ENV.fetch("SLACK_WEBHOOK_URL", nil).presence || abort("SLACK_WEBHOOK_URL is not set")
9
+ end
10
+
11
+ # The color bar on the side comes from the (legacy) attachment, which an incoming webhook still renders.
12
+ def post(text, color:)
13
+ attachment = { color: color, text: text, mrkdwn_in: ["text"] }
14
+ payload = SENDER.merge(channel: @channel, attachments: [attachment]).to_json
15
+ response = Net::HTTP.post(URI(@url), payload, "Content-Type" => "application/json")
16
+ return if response.is_a?(Net::HTTPSuccess)
17
+
18
+ abort "Slack refused the notification: #{response.code} #{response.body}"
19
+ end
20
+ end
@@ -43,4 +43,4 @@ blocks:
43
43
  # - renuo review-app:deploy --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --origin-pattern <%= origin_pattern %> --keep-envs=build,sensitive-build,runtime,sensitive-runtime --start
44
44
  - renuo review-app:start --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
45
45
  - renuo ci check-deploio-status $(renuo review-app:name --branch $SEMAPHORE_GIT_WORKING_BRANCH) --project <%= project %>
46
- - renuo review-app:notify --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --channel '<%= slack_channel %>'
46
+ - renuo notify:slack:review-app-deploy --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --channel '<%= slack_channel %>'
@@ -19,3 +19,4 @@ blocks:
19
19
  - gem i -q renuo-cli
20
20
  - renuo ci setup
21
21
  - renuo review-app:purge --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
22
+ - renuo notify:slack:review-app-purge --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --channel '<%= slack_channel %>'
@@ -13,7 +13,7 @@ blocks:
13
13
  task:
14
14
  secrets:
15
15
  - name: <%= project_name %>
16
- - name: nctl
16
+ - name: <%= nctl_secret_name %>
17
17
  jobs:
18
18
  - name: deploying
19
19
  commands:
@@ -3,7 +3,7 @@
3
3
  # simplecov:disable
4
4
  module Renuo
5
5
  class Cli
6
- VERSION = "4.30.0"
6
+ VERSION = "5.0.0"
7
7
  NAME = "renuo-cli"
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: renuo-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.30.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renuo AG
@@ -116,11 +116,12 @@ files:
116
116
  - lib/renuo/cli/commands/github_replace.rb
117
117
  - lib/renuo/cli/commands/heroku_users.rb
118
118
  - lib/renuo/cli/commands/list_large_git_files.rb
119
+ - lib/renuo/cli/commands/notify/slack/review_app_deploy.rb
120
+ - lib/renuo/cli/commands/notify/slack/review_app_purge.rb
119
121
  - lib/renuo/cli/commands/release.rb
120
122
  - lib/renuo/cli/commands/review_app/README.md
121
123
  - lib/renuo/cli/commands/review_app/deploy.rb
122
124
  - lib/renuo/cli/commands/review_app/name.rb
123
- - lib/renuo/cli/commands/review_app/notify.rb
124
125
  - lib/renuo/cli/commands/review_app/purge.rb
125
126
  - lib/renuo/cli/commands/review_app/setup_semaphoreci.rb
126
127
  - lib/renuo/cli/commands/review_app/start.rb
@@ -136,6 +137,7 @@ files:
136
137
  - lib/renuo/cli/helpers/environments.rb
137
138
  - lib/renuo/cli/helpers/renuo_version.rb
138
139
  - lib/renuo/cli/helpers/review_app_helper.rb
140
+ - lib/renuo/cli/helpers/review_app_notification.rb
139
141
  - lib/renuo/cli/helpers/safe_review_app_name_contract.rb
140
142
  - lib/renuo/cli/helpers/zsh_completion_script.rb
141
143
  - lib/renuo/cli/services/cache.rb
@@ -153,6 +155,7 @@ files:
153
155
  - lib/renuo/cli/services/heroku.rb
154
156
  - lib/renuo/cli/services/hetzner.rb
155
157
  - lib/renuo/cli/services/renuo_cli_config.rb
158
+ - lib/renuo/cli/services/review_app_slack_webhook.rb
156
159
  - lib/renuo/cli/templates/semaphore/bin/cache_restore.erb
157
160
  - lib/renuo/cli/templates/semaphore/bin/cache_store.erb
158
161
  - lib/renuo/cli/templates/semaphore/notification.yml.erb
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../helpers/safe_review_app_name_contract"
4
-
5
- module Renuo::Cli::Commands::ReviewApp
6
- class Notify
7
- include ReviewAppHelper
8
- include Renuo::Cli::Services
9
- include Contracts::Core
10
-
11
- command "review-app:notify" do |c|
12
- c.syntax = "renuo review-app:notify [options]"
13
- c.summary = "Posts the branch's review app URL (with its basic auth credentials) and pull request to Slack."
14
- c.description = "#{c.summary} The Slack incoming webhook is read from SLACK_WEBHOOK_URL, the GitHub " \
15
- "repository from SEMAPHORE_GIT_REPO_SLUG and bundler's GitHub credentials (a token or " \
16
- "user:token) from BUNDLE_GITHUB__COM to link the branch's open pull request."
17
- c.option "-p", "--project <name>", String, "The name of the Deploio project"
18
- c.option "-b", "--branch <name>", String, "The Git branch"
19
- c.option "--channel <name>", String, "The Slack channel to post to"
20
- c.option "--repo <owner/name>", String, "The GitHub repository, defaults to SEMAPHORE_GIT_REPO_SLUG"
21
- c.example "renuo review-app:notify -p renuo-acme -b feature/42-migrate-to-deploio " \
22
- "--channel '#project-acme-notifications'",
23
- "Announces the branch's review app in the project's Slack channel"
24
- c.action { |_args, options| new(options).run }
25
- end
26
-
27
- def run
28
- post_to_slack(message_lines.join("\n"))
29
- CmdTrail::Print.call
30
- puts "\nnotified #{@channel}"
31
- end
32
-
33
- private
34
-
35
- def initialize(options)
36
- @project = options.project || abort("missing project name")
37
- @branch = options.branch || abort("missing branch name")
38
- @channel = options.channel || abort("missing Slack channel")
39
- @repo = options.repo || ENV.fetch("SEMAPHORE_GIT_REPO_SLUG", nil).presence ||
40
- abort("missing GitHub repository, set SEMAPHORE_GIT_REPO_SLUG or pass --repo")
41
- @webhook_url = ENV.fetch("SLACK_WEBHOOK_URL", nil).presence || abort("SLACK_WEBHOOK_URL is not set")
42
- derive_app_name(@branch)
43
- end
44
-
45
- Contract String => SafeReviewAppNameContract
46
- def derive_app_name(branch)
47
- @app_name = review_app_name(branch)
48
- end
49
-
50
- def message_lines
51
- credentials = basic_auth_credentials
52
- pull_request = fetch_pull_request
53
-
54
- [
55
- "Review app is ready: #{app_link(credentials)}",
56
- pull_request && pull_request_line(pull_request),
57
- "Branch: <https://github.com/#{@repo}/tree/#{@branch}|#{@branch}>"
58
- ].compact
59
- end
60
-
61
- def pull_request_line(pull_request)
62
- "Pull request: <#{pull_request["html_url"]}|##{pull_request["number"]} #{pull_request["title"]}>"
63
- end
64
-
65
- def app_link(credentials)
66
- url = app_url
67
- url.userinfo = userinfo(credentials) if credentials
68
- "<#{url}|#{url.host}>"
69
- end
70
-
71
- def userinfo(credentials)
72
- credentials.values_at("username", "password").map { |value| URI.encode_www_form_component(value) }.join(":")
73
- end
74
-
75
- def app_url
76
- url = Deploio.fetch_resource("app", @app_name, @project).to_h.dig("status", "atProvider", "defaultURLs", 0)
77
- URI(url || abort("#{@app_name} has no URL assigned yet"))
78
- end
79
-
80
- # Deploio assigns basic auth credentials per app, so the review app's differ from the origin's.
81
- def basic_auth_credentials
82
- stdout, = system!("nctl get app #{@app_name} -p #{@project} -o json --basic-auth-credentials", quiet: true)
83
- Array(JSON.parse(stdout)).first&.dig("basicauth")
84
- end
85
-
86
- def fetch_pull_request
87
- credentials = ENV.fetch("BUNDLE_GITHUB__COM", "")
88
- return if credentials.empty?
89
-
90
- response = Net::HTTP.start("api.github.com", 443, use_ssl: true) do |http|
91
- http.request(pull_request_request(credentials))
92
- end
93
- return JSON.parse(response.body).first if response.is_a?(Net::HTTPSuccess)
94
-
95
- warn "GitHub pull request lookup failed: #{response.code}"
96
- nil
97
- end
98
-
99
- def pull_request_request(credentials)
100
- query = URI.encode_www_form(state: "open", head: "#{@repo.split("/").first}:#{@branch}")
101
- request = Net::HTTP::Get.new("/repos/#{@repo}/pulls?#{query}", "Accept" => "application/vnd.github+json")
102
- user, token = credentials.split(":", 2)
103
- request.basic_auth(user, token || "x-oauth-basic")
104
- request
105
- end
106
-
107
- SENDER = { username: "Deploio Review Apps", icon_url: "https://deploio.ch/meta/apple-touch-icon.png" }.freeze
108
- DEPLOIO_BLUE = "#141d50"
109
-
110
- # The color bar on the side comes from the (legacy) attachment, which an incoming webhook still renders.
111
- def post_to_slack(text)
112
- attachment = { color: DEPLOIO_BLUE, text: text, mrkdwn_in: ["text"] }
113
- payload = SENDER.merge(channel: @channel, attachments: [attachment]).to_json
114
- response = Net::HTTP.post(URI(@webhook_url), payload, "Content-Type" => "application/json")
115
- return if response.is_a?(Net::HTTPSuccess)
116
-
117
- abort "Slack refused the notification: #{response.code} #{response.body}"
118
- end
119
- end
120
- end