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 +4 -4
- data/lib/renuo/cli/commands/configure_semaphore.rb +37 -0
- data/lib/renuo/cli/commands/create_deploio_app.rb +1 -1
- data/lib/renuo/cli/commands/notify/slack/review_app_deploy.rb +88 -0
- data/lib/renuo/cli/commands/notify/slack/review_app_purge.rb +32 -0
- data/lib/renuo/cli/commands/review_app/README.md +7 -3
- data/lib/renuo/cli/commands/review_app/setup_semaphoreci.rb +5 -3
- data/lib/renuo/cli/helpers/review_app_notification.rb +53 -0
- data/lib/renuo/cli/services/review_app_slack_webhook.rb +20 -0
- data/lib/renuo/cli/templates/semaphore/review-app-deploy.yml.erb +1 -1
- data/lib/renuo/cli/templates/semaphore/review-app-purge.yml.erb +1 -0
- data/lib/renuo/cli/templates/semaphore/semaphore-deploy.yml.erb +1 -1
- data/lib/renuo/cli/version.rb +1 -1
- metadata +5 -2
- data/lib/renuo/cli/commands/review_app/notify.rb +0 -120
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8462a65e3075cb5e8a3f7c01a254ac225e81ce774ab53d16a1915883e6d4969a
|
|
4
|
+
data.tar.gz: 4985892734d7db16156a8139c325779c4d60f8763de9b7d690cac7b91503ce77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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 %>'
|
data/lib/renuo/cli/version.rb
CHANGED
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
|
+
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
|