renuo-cli 4.29.1 → 4.30.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/ci/check_deploio_status.rb +1 -1
- data/lib/renuo/cli/commands/release.rb +2 -1
- data/lib/renuo/cli/commands/review_app/README.md +4 -0
- data/lib/renuo/cli/commands/review_app/notify.rb +120 -0
- data/lib/renuo/cli/commands/review_app/setup_semaphoreci.rb +14 -6
- data/lib/renuo/cli/helpers/credentials_helper.rb +11 -7
- data/lib/renuo/cli/templates/semaphore/bin/cache_restore.erb +4 -0
- data/lib/renuo/cli/templates/semaphore/bin/cache_store.erb +9 -0
- data/lib/renuo/cli/templates/semaphore/review-app-deploy.yml.erb +2 -0
- data/lib/renuo/cli/templates/semaphore/semaphore.yml.erb +2 -0
- data/lib/renuo/cli/version.rb +1 -1
- data/lib/renuo/cli.rb +8 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e403749b46f9d289d45f218209831c68a8c624840f4df20e1315127d4ba2bbac
|
|
4
|
+
data.tar.gz: 85cd1b725f516ab96fa5da67355c9268d8397eeb3b7db4017263b3583558f4d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 193344bdebb9952145a2c8e948e04b8ad85f8d04d0d642c8c0f00cd1173639454c7306a7c80665de5bcc68c7e78a5de74f4406012de7e4d61cda5d0a9a173211
|
|
7
|
+
data.tar.gz: c6bd00ecd6bedcde704ab346dcfa3cdd0affae38a0413c457636c575c561b12432a9a06c1422de2673b0ff0a40bb8c7e03f66626321d7c756eb0e8ca5927ae0a
|
|
@@ -201,7 +201,8 @@ class Renuo::Cli::Commands::Release # rubocop:disable Metrics/ClassLength
|
|
|
201
201
|
abort(">> Cancelling Release.")
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
now = Time.now.getlocal
|
|
205
|
+
return unless now.friday? && now.hour >= 16
|
|
205
206
|
|
|
206
207
|
unless agree(">> Are you sure you want to deploy on late Friday afternoon? " \
|
|
207
208
|
"Did you think about your family...waiting for you at home?")
|
|
@@ -13,6 +13,7 @@ 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
17
|
1. Cleanup `renuo review-app:purge`
|
|
17
18
|
|
|
18
19
|
## Design Choices
|
|
@@ -27,3 +28,6 @@ These are the steps of command execution in the happy path:
|
|
|
27
28
|
* All `nctl` commands are routed through `system!` and are logged in a command trail for auditing.
|
|
28
29
|
* Configuration of system tools like `psql` is intentionally out of scope.
|
|
29
30
|
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
|
|
33
|
+
bundler's `BUNDLE_GITHUB__COM` (a token or `user:token`) and never fails the pipeline when GitHub refuses it.
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
|
@@ -8,7 +8,7 @@ module Renuo::Cli::Commands::ReviewApp
|
|
|
8
8
|
DEPLOY_PIPELINE_TARGET_FILE = ".semaphore/review-app-deploy.yml"
|
|
9
9
|
PURGE_PIPELINE_TARGET_FILE = ".semaphore/review-app-purge.yml"
|
|
10
10
|
|
|
11
|
-
attr_reader :project, :origin_pattern
|
|
11
|
+
attr_reader :project, :origin_pattern, :slack_channel
|
|
12
12
|
|
|
13
13
|
command "review-app:setup-semaphoreci" do |c|
|
|
14
14
|
c.syntax = "renuo review-app:setup-semaphoreci [options]"
|
|
@@ -16,13 +16,15 @@ 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
20
|
c.action { |_args, options| new(options).run }
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def run
|
|
23
24
|
FileUtils.mkdir_p File.dirname(DEPLOY_PIPELINE_TARGET_FILE)
|
|
24
25
|
write_or_warn DEPLOY_PIPELINE_TARGET_FILE,
|
|
25
|
-
render(DEPLOY_PIPELINE_TEMPLATE_FILE,
|
|
26
|
+
render(DEPLOY_PIPELINE_TEMPLATE_FILE,
|
|
27
|
+
{ project:, origin_pattern:, secret_name:, slack_channel: })
|
|
26
28
|
write_or_warn PURGE_PIPELINE_TARGET_FILE, render(PURGE_PIPELINE_TEMPLATE_FILE, { project:, secret_name: })
|
|
27
29
|
print_instructions
|
|
28
30
|
end
|
|
@@ -32,10 +34,11 @@ module Renuo::Cli::Commands::ReviewApp
|
|
|
32
34
|
def initialize(options)
|
|
33
35
|
@project = options.project || abort("missing project name")
|
|
34
36
|
@origin_pattern = options.origin_pattern || abort("missing origin pattern")
|
|
37
|
+
@slack_channel = options.slack_channel || abort("missing Slack channel")
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def secret_name
|
|
38
|
-
"
|
|
41
|
+
"review-apps"
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
# Deploio project names are prefixed with the name of their organization.
|
|
@@ -51,12 +54,17 @@ module Renuo::Cli::Commands::ReviewApp
|
|
|
51
54
|
|
|
52
55
|
nctl create asa #{service_account_name} --project #{project}
|
|
53
56
|
|
|
54
|
-
Register its credentials
|
|
57
|
+
Register its credentials and the incoming webhook of the Slack channel #{slack_channel}
|
|
58
|
+
(https://api.slack.com/messaging/webhooks) as the project secret #{secret_name} of the Semaphore project
|
|
59
|
+
(assumed to be named #{project} as well), used by both pipelines:
|
|
55
60
|
|
|
56
|
-
sem create secret #{secret_name} \\
|
|
61
|
+
sem create secret -p #{project} #{secret_name} \\
|
|
57
62
|
-e NCTL_API_CLIENT_ID="$(nctl get asa #{service_account_name} --project #{project} --print-client-id)" \\
|
|
58
63
|
-e NCTL_API_CLIENT_SECRET="$(nctl get asa #{service_account_name} --project #{project} --print-client-secret)" \\
|
|
59
|
-
-e NCTL_ORGANIZATION=#{organization}
|
|
64
|
+
-e NCTL_ORGANIZATION=#{organization} \\
|
|
65
|
+
-e SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
|
66
|
+
|
|
67
|
+
The deploy pipeline also reads the bundle-github secret: its BUNDLE_GITHUB__COM links the branch's pull request.
|
|
60
68
|
|
|
61
69
|
Add these promotions to .semaphore/semaphore.yml (no auto_promote, so they only run on click):
|
|
62
70
|
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_support/encrypted_configuration"
|
|
4
|
-
|
|
5
3
|
module CredentialsHelper
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
# required lazily, it takes a large share of the CLI's startup time
|
|
5
|
+
def self.config
|
|
6
|
+
@config ||= begin
|
|
7
|
+
require "active_support/encrypted_configuration"
|
|
8
|
+
ActiveSupport::EncryptedConfiguration.new(
|
|
9
|
+
config_path: File.expand_path("../credentials.yml.enc", __dir__),
|
|
10
|
+
key_path: "", env_key: "RENUO_CLI_MASTER_KEY", raise_if_missing_key: true
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
10
14
|
|
|
11
15
|
def credentials
|
|
12
|
-
|
|
16
|
+
CredentialsHelper.config.config
|
|
13
17
|
end
|
|
14
18
|
end
|
|
@@ -20,6 +20,10 @@ do
|
|
|
20
20
|
cache restore "sprocketscache-$SEMAPHORE_GIT_BRANCH,sprocketscache-develop"
|
|
21
21
|
;;
|
|
22
22
|
|
|
23
|
+
run_times)
|
|
24
|
+
cache restore "rspec-run-times-$SEMAPHORE_GIT_BRANCH,rspec-run-times-develop,rspec-run-times-main"
|
|
25
|
+
;;
|
|
26
|
+
|
|
23
27
|
*)
|
|
24
28
|
echo "Invalid argument: Cache group $cache_group not found."
|
|
25
29
|
exit 1
|
|
@@ -20,6 +20,15 @@ do
|
|
|
20
20
|
cache store sprocketscache-$SEMAPHORE_GIT_BRANCH tmp/cache/assets/sprockets
|
|
21
21
|
;;
|
|
22
22
|
|
|
23
|
+
# rspec-core-parallel run times change with every build, so the key is replaced.
|
|
24
|
+
# Only main and develop store them, the other branches start from theirs.
|
|
25
|
+
run_times)
|
|
26
|
+
if [ -f tmp/rspec_parallel_run_times.txt ] && { [ "$SEMAPHORE_GIT_BRANCH" = "main" ] || [ "$SEMAPHORE_GIT_BRANCH" = "develop" ]; }; then
|
|
27
|
+
cache delete rspec-run-times-$SEMAPHORE_GIT_BRANCH
|
|
28
|
+
cache store rspec-run-times-$SEMAPHORE_GIT_BRANCH tmp/rspec_parallel_run_times.txt
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
|
|
23
32
|
*)
|
|
24
33
|
echo "Invalid argument: Cache group $cache_group not found."
|
|
25
34
|
exit 1
|
|
@@ -13,6 +13,7 @@ blocks:
|
|
|
13
13
|
task:
|
|
14
14
|
secrets:
|
|
15
15
|
- name: <%= secret_name %>
|
|
16
|
+
- name: bundle-github
|
|
16
17
|
jobs:
|
|
17
18
|
- name: deploying
|
|
18
19
|
commands:
|
|
@@ -42,3 +43,4 @@ blocks:
|
|
|
42
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
|
|
43
44
|
- renuo review-app:start --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
|
|
44
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 %>'
|
|
@@ -53,10 +53,12 @@ blocks:
|
|
|
53
53
|
- sem-service start postgres 18
|
|
54
54
|
- cp .env.example .env
|
|
55
55
|
- bundle exec rails db:create db:schema:load
|
|
56
|
+
- source .semaphore/bin/cache_restore run_times
|
|
56
57
|
jobs:
|
|
57
58
|
- name: tests
|
|
58
59
|
commands:
|
|
59
60
|
- bin/check
|
|
61
|
+
- source .semaphore/bin/cache_store run_times
|
|
60
62
|
epilogue:
|
|
61
63
|
always:
|
|
62
64
|
commands:
|
data/lib/renuo/cli/version.rb
CHANGED
data/lib/renuo/cli.rb
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support"
|
|
4
|
-
require "active_support/core_ext"
|
|
4
|
+
require "active_support/core_ext/enumerable"
|
|
5
|
+
require "active_support/core_ext/hash/keys"
|
|
6
|
+
require "active_support/core_ext/object/blank"
|
|
7
|
+
require "active_support/core_ext/string/exclude"
|
|
8
|
+
require "active_support/core_ext/string/filters"
|
|
9
|
+
require "active_support/core_ext/string/inflections"
|
|
5
10
|
require "colorize"
|
|
6
11
|
require "fileutils"
|
|
7
12
|
require "highline"
|
|
8
13
|
require "json"
|
|
9
14
|
require "net/http"
|
|
10
15
|
require "open3"
|
|
11
|
-
require "psych"
|
|
12
16
|
require "securerandom"
|
|
13
17
|
require "service_actor"
|
|
18
|
+
require "time"
|
|
14
19
|
require "tmpdir"
|
|
15
20
|
require "uri"
|
|
21
|
+
require "yaml"
|
|
16
22
|
require "renuo/cli/version"
|
|
17
23
|
|
|
18
24
|
module Renuo
|
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.
|
|
4
|
+
version: 4.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Renuo AG
|
|
@@ -120,6 +120,7 @@ files:
|
|
|
120
120
|
- lib/renuo/cli/commands/review_app/README.md
|
|
121
121
|
- lib/renuo/cli/commands/review_app/deploy.rb
|
|
122
122
|
- lib/renuo/cli/commands/review_app/name.rb
|
|
123
|
+
- lib/renuo/cli/commands/review_app/notify.rb
|
|
123
124
|
- lib/renuo/cli/commands/review_app/purge.rb
|
|
124
125
|
- lib/renuo/cli/commands/review_app/setup_semaphoreci.rb
|
|
125
126
|
- lib/renuo/cli/commands/review_app/start.rb
|