renuo-cli 4.26.0 → 4.28.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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/lib/renuo/cli/commands/ci/check_deploio_status.rb +3 -3
  3. data/lib/renuo/cli/commands/review_app/deploy.rb +114 -0
  4. data/lib/renuo/cli/commands/review_app/name.rb +38 -0
  5. data/lib/renuo/cli/commands/review_app/purge.rb +67 -0
  6. data/lib/renuo/cli/commands/review_app/setup_semaphoreci.rb +87 -0
  7. data/lib/renuo/cli/commands/review_app/start.rb +53 -0
  8. data/lib/renuo/cli/helpers/actor_contracts.rb +16 -0
  9. data/lib/renuo/cli/helpers/cmd_trail.rb +45 -0
  10. data/lib/renuo/cli/helpers/review_app_helper.rb +46 -0
  11. data/lib/renuo/cli/helpers/safe_review_app_name_contract.rb +17 -0
  12. data/lib/renuo/cli/services/deploio/copy_app.rb +55 -0
  13. data/lib/renuo/cli/services/deploio/copy_database_data.rb +57 -0
  14. data/lib/renuo/cli/services/deploio/copy_s3_data.rb +70 -0
  15. data/lib/renuo/cli/services/deploio/create_database.rb +49 -0
  16. data/lib/renuo/cli/services/deploio/create_kvs.rb +41 -0
  17. data/lib/renuo/cli/services/deploio/create_s3.rb +82 -0
  18. data/lib/renuo/cli/services/deploio/inventorize_origin_resources.rb +174 -0
  19. data/lib/renuo/cli/services/deploio/start_target_app.rb +19 -0
  20. data/lib/renuo/cli/services/deploio/update_target_app_env.rb +37 -0
  21. data/lib/renuo/cli/services/deploio.rb +16 -4
  22. data/lib/renuo/cli/templates/semaphore/review-app-deploy.yml.erb +44 -0
  23. data/lib/renuo/cli/templates/semaphore/review-app-purge.yml.erb +21 -0
  24. data/lib/renuo/cli/templates/semaphore/semaphore.yml.erb +1 -0
  25. data/lib/renuo/cli/version.rb +1 -1
  26. data/lib/renuo/cli.rb +10 -0
  27. metadata +49 -1
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "create_s3"
4
+
5
+ class Renuo::Cli::Services::Deploio
6
+ class CopyS3Data < Actor
7
+ include ReviewAppHelper
8
+
9
+ DATA_DIR = File.join(Dir.tmpdir, "review-app-bucket-data")
10
+ LISTING_ATTEMPTS = 24
11
+ LISTING_INTERVAL_IN_SECONDS = 5
12
+
13
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
14
+ input :s3_env
15
+ input :origin_bucket_name
16
+ input :origin_bucket_endpoint
17
+ input :origin_bucket_access_key
18
+ input :origin_bucket_secret_key
19
+
20
+ def call
21
+ return puts "bucket #{target_app_name} already holds objects, skipping data copy" unless bucket_empty?
22
+
23
+ puts "copying bucket data from #{origin_bucket_name} into #{target_app_name}..."
24
+ download_data
25
+ upload_data
26
+ end
27
+
28
+ private
29
+
30
+ def download_data
31
+ system! "aws s3 sync s3://#{origin_bucket_name} #{DATA_DIR} --endpoint-url https://#{origin_bucket_endpoint}",
32
+ aws_credentials(origin_bucket_access_key, origin_bucket_secret_key)
33
+ end
34
+
35
+ # aws exits non-zero on some empty listings, so only stderr tells a failure apart.
36
+ def bucket_empty?
37
+ stdout, stderr = list_target_bucket
38
+ abort "'aws s3' failed: #{stderr}" unless stderr.strip.empty?
39
+ stdout.strip.empty?
40
+ end
41
+
42
+ # Deploio reports a fresh bucket ready seconds before its permissions grant access.
43
+ def list_target_bucket
44
+ (1..LISTING_ATTEMPTS).each do |attempt|
45
+ stdout, stderr, = system!("aws s3 ls s3://#{target_app_name} --endpoint-url #{target_endpoint}",
46
+ target_credentials, quiet: true, abort_on_failure: false)
47
+ return [stdout, stderr] if attempt == LISTING_ATTEMPTS || stderr.exclude?("AccessDenied")
48
+
49
+ puts "waiting for access to bucket #{target_app_name}..."
50
+ Kernel.sleep(LISTING_INTERVAL_IN_SECONDS)
51
+ end
52
+ end
53
+
54
+ def upload_data
55
+ system! "aws s3 sync #{DATA_DIR} s3://#{target_app_name} --endpoint-url #{target_endpoint}", target_credentials
56
+ end
57
+
58
+ def target_endpoint
59
+ s3_env.fetch(target_schema[:endpoint])
60
+ end
61
+
62
+ def target_credentials
63
+ aws_credentials(s3_env.fetch(target_schema[:access_key]), s3_env.fetch(target_schema[:secret_key]))
64
+ end
65
+
66
+ def target_schema
67
+ CreateS3::ENV_SCHEMAS_IN_THE_WILD.find { |candidate| s3_env.key?(candidate[:secret_key]) }
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class Renuo::Cli::Services::Deploio
6
+ class CreateDatabase < Actor
7
+ include ReviewAppHelper
8
+ include Renuo::Cli::Services
9
+
10
+ input :project
11
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
12
+ input :origin_database_location, allow_nil: true, default: nil
13
+ input :origin_database_version, allow_nil: true, default: nil
14
+ input :origin_database_collation, allow_nil: true, default: nil
15
+
16
+ output :database_env
17
+
18
+ def call
19
+ if Deploio.resource_exists?("postgresdatabase", target_app_name, project)
20
+ puts "database #{target_app_name} already exists, skipping creation"
21
+ else
22
+ create_database
23
+ end
24
+ self.database_env = { "DATABASE_URL" => database_url }
25
+ end
26
+
27
+ private
28
+
29
+ def create_database
30
+ puts "creating database #{target_app_name}..."
31
+ system! ["nctl create postgresdatabase #{target_app_name} -p #{project}",
32
+ *origin_flags,
33
+ "--backup-schedule=disabled --wait"].join(" ")
34
+ end
35
+
36
+ def origin_flags
37
+ { "--location" => origin_database_location,
38
+ "--postgres-database-version" => origin_database_version,
39
+ "--collation" => origin_database_collation }
40
+ .filter_map { |flag, value| "#{flag} #{Shellwords.escape(value)}" if value }
41
+ end
42
+
43
+ def database_url
44
+ stdout, = system!("nctl get postgresdatabase #{target_app_name} -p #{project} --print-connection-string",
45
+ quiet: true)
46
+ stdout.strip
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class Renuo::Cli::Services::Deploio
6
+ class CreateKvs < Actor
7
+ include ReviewAppHelper
8
+ include Renuo::Cli::Services
9
+
10
+ input :project
11
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
12
+ input :origin_key_value_store_location, allow_nil: true, default: nil
13
+
14
+ output :kvs_env
15
+
16
+ def call
17
+ if Deploio.resource_exists?("kvs", target_app_name, project)
18
+ puts "key-value store #{target_app_name} already exists, skipping creation"
19
+ else
20
+ create_kvs
21
+ end
22
+ self.kvs_env = { "REDIS_URL" => redis_url }
23
+ end
24
+
25
+ private
26
+
27
+ def create_kvs
28
+ system! ["nctl create kvs #{target_app_name} -p #{project}", location_flag, "--wait"].compact.join(" ")
29
+ end
30
+
31
+ def location_flag
32
+ "--location #{Shellwords.escape(origin_key_value_store_location)}" if origin_key_value_store_location
33
+ end
34
+
35
+ def redis_url
36
+ token, = system!("nctl get kvs #{target_app_name} -p #{project} --print-token", quiet: true)
37
+ fqdn = Deploio.fetch_resource("kvs", target_app_name, project).dig("status", "atProvider", "fqdn")
38
+ "rediss://:#{token.strip}@#{fqdn}:6379"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renuo::Cli::Services::Deploio
4
+ class CreateS3 < Actor
5
+ include ReviewAppHelper
6
+ include Renuo::Cli::Services
7
+
8
+ DEFAULT_BUCKET_LOCATION = "nine-es34"
9
+ ENV_SCHEMAS_IN_THE_WILD = [
10
+ {
11
+ name: "DEPLOIO_BUCKET_NAME",
12
+ endpoint: "DEPLOIO_BUCKET_ENDPOINT",
13
+ access_key: "DEPLOIO_BUCKET_ACCESS_KEY",
14
+ secret_key: "DEPLOIO_BUCKET_SECRET_KEY"
15
+ },
16
+ {
17
+ name: "DEPLOIO_BUCKET",
18
+ endpoint: "DEPLOIO_ENDPOINT",
19
+ access_key: "DEPLOIO_ACCESS_KEY",
20
+ secret_key: "DEPLOIO_SECRET_KEY"
21
+ }
22
+ ].freeze
23
+ SENSITIVE_ENV_KEYS = ENV_SCHEMAS_IN_THE_WILD.flat_map { |schema| schema.values_at(:access_key, :secret_key) }.freeze
24
+
25
+ input :project
26
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
27
+ input :template, default: {}
28
+
29
+ output :s3_env
30
+
31
+ def call
32
+ location = template[:bucket_location] || DEFAULT_BUCKET_LOCATION
33
+ create_bucket_user(location)
34
+ create_bucket(location)
35
+ self.s3_env = bucket_env
36
+ end
37
+
38
+ private
39
+
40
+ def app_env?(key)
41
+ (template[:app_env] || []).any? { |variable| variable["name"] == key }
42
+ end
43
+
44
+ def create_bucket_user(location)
45
+ return if Deploio.resource_exists?("bucketuser", target_app_name, project)
46
+
47
+ system! "nctl create bucketuser #{target_app_name} -p #{project} --location #{location} --wait"
48
+ end
49
+
50
+ def create_bucket(location)
51
+ if Deploio.resource_exists?("bucket", target_app_name, project)
52
+ return puts "bucket #{target_app_name} already exists, skipping creation"
53
+ end
54
+
55
+ system! "nctl create bucket #{target_app_name} -p #{project} --location #{location} " \
56
+ "--permissions \"reader=#{target_app_name};writer=#{target_app_name}\" --wait"
57
+ end
58
+
59
+ def bucket_env
60
+ schema = env_schema
61
+ {
62
+ schema[:name] => target_app_name,
63
+ schema[:endpoint] => "https://#{bucket_endpoint}",
64
+ schema[:access_key] => bucket_credential("--print-access-key"),
65
+ schema[:secret_key] => bucket_credential("--print-secret-key")
66
+ }
67
+ end
68
+
69
+ def bucket_endpoint
70
+ Deploio.fetch_resource("bucket", target_app_name, project).dig("status", "atProvider", "endpoint")
71
+ end
72
+
73
+ def bucket_credential(flag)
74
+ stdout, = system!("nctl get bucketuser #{target_app_name} -p #{project} #{flag}", quiet: true)
75
+ stdout.strip
76
+ end
77
+
78
+ def env_schema
79
+ ENV_SCHEMAS_IN_THE_WILD.find { |schema| app_env?(schema[:secret_key]) } || ENV_SCHEMAS_IN_THE_WILD.first
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Renuo::Cli::Services::Deploio
4
+ class InventorizeOriginResources < Actor # rubocop:disable Metrics/ClassLength
5
+ include ReviewAppHelper
6
+
7
+ input :project
8
+ input :origin_pattern,
9
+ must: {
10
+ be_specific_enough: {
11
+ is: ->(pattern) { pattern.to_s.length >= ReviewAppHelper::MIN_PATTERN_LENGTH },
12
+ message: (lambda do |value:, **|
13
+ "origin pattern '#{value}' must be at least #{ReviewAppHelper::MIN_PATTERN_LENGTH} " \
14
+ "characters long, aborting"
15
+ end)
16
+ }
17
+ }
18
+
19
+ output :origin_app_name
20
+ output :origin_database_name
21
+ output :origin_database_location
22
+ output :origin_database_version
23
+ output :origin_database_collation
24
+ output :origin_backup_bucket
25
+ output :origin_backup_path
26
+ output :origin_backup_endpoint
27
+ output :origin_backup_bucket_user
28
+ output :origin_backup_access_key
29
+ output :origin_backup_secret_key
30
+ output :origin_key_value_store_name
31
+ output :origin_key_value_store_location
32
+ output :origin_bucket_name
33
+ output :origin_bucket_endpoint
34
+ output :origin_bucket_user
35
+ output :origin_bucket_access_key
36
+ output :origin_bucket_secret_key
37
+
38
+ def call
39
+ inventorize_app
40
+ inventorize_database
41
+ inventorize_backup
42
+ inventorize_key_value_store
43
+ inventorize_bucket
44
+ ensure_required_tools
45
+ end
46
+
47
+ private
48
+
49
+ def inventorize_app
50
+ self.origin_app_name = origin_app_name!
51
+ end
52
+
53
+ def inventorize_database
54
+ database = origin_resource("PostgresDatabase")
55
+ self.origin_database_name = database&.dig("metadata", "name")
56
+ self.origin_database_location, self.origin_database_version, self.origin_database_collation =
57
+ %w[location version collation].map { |key| database&.dig("spec", "forProvider", key)&.to_s }
58
+ end
59
+
60
+ def inventorize_backup
61
+ self.origin_backup_bucket = backup_bucket_name
62
+ self.origin_backup_path = latest_backup_path
63
+ backup_bucket = bucket_resource(origin_backup_bucket)
64
+ self.origin_backup_endpoint = backup_bucket&.dig("status", "atProvider", "endpoint")
65
+ self.origin_backup_bucket_user = bucket_user(backup_bucket)
66
+ self.origin_backup_access_key, self.origin_backup_secret_key =
67
+ bucketuser_credentials(origin_backup_bucket_user)
68
+ end
69
+
70
+ def bucket_user(bucket)
71
+ return nil unless bucket
72
+
73
+ bucket_reader_user(bucket) || abort("no bucket user for '#{bucket.dig("metadata", "name")}' in #{project}")
74
+ end
75
+
76
+ # Bucket users are not necessarily named like their bucket; the bucket's permissions reference them.
77
+ def bucket_reader_user(bucket)
78
+ permissions = bucket.dig("spec", "forProvider", "permissions") || []
79
+ permission = permissions.find { |candidate| candidate["role"] == "reader" } || permissions.first
80
+ permission&.dig("userRefs", 0, "name")
81
+ end
82
+
83
+ # Fetched eagerly (even for re-deploys that copy no data) so no origin-side nctl calls remain after inventory.
84
+ def bucketuser_credentials(user)
85
+ return [nil, nil] unless user
86
+
87
+ %w[--print-access-key --print-secret-key].map do |flag|
88
+ stdout, = system!("nctl get bucketuser #{user} -p #{project} #{flag}", quiet: true)
89
+ stdout.strip
90
+ end
91
+ end
92
+
93
+ def inventorize_key_value_store
94
+ key_value_store = origin_resource("KeyValueStore")
95
+ self.origin_key_value_store_name = key_value_store&.dig("metadata", "name")
96
+ self.origin_key_value_store_location = key_value_store&.dig("spec", "forProvider", "location")
97
+ end
98
+
99
+ def inventorize_bucket
100
+ bucket = origin_resource("Bucket")
101
+ self.origin_bucket_name = bucket&.dig("metadata", "name")
102
+ self.origin_bucket_endpoint = bucket&.dig("status", "atProvider", "endpoint")
103
+ self.origin_bucket_user = bucket_user(bucket)
104
+ self.origin_bucket_access_key, self.origin_bucket_secret_key = bucketuser_credentials(origin_bucket_user)
105
+ end
106
+
107
+ def ensure_required_tools
108
+ missing = required_tools.reject { |tool| installed?(tool) }
109
+ abort "missing #{missing.join(", ")} to copy the origin data, install them and rerun" if missing.any?
110
+ end
111
+
112
+ def required_tools
113
+ tools = []
114
+ tools += %w[aws pg_restore psql zstd] if origin_backup_path
115
+ tools += %w[aws] if origin_bucket_name
116
+ tools.uniq
117
+ end
118
+
119
+ def installed?(tool)
120
+ ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, tool)) }
121
+ end
122
+
123
+ def resources
124
+ # nctl hides the whole listing behind its no-resources banner when the project holds
125
+ # no nine-owned resources; the plain listing still contains everything else.
126
+ @resources ||= nctl_resources("nctl get all -p #{project} --include-nine-resources -o yaml").presence ||
127
+ nctl_resources("nctl get all -p #{project} -o yaml")
128
+ end
129
+
130
+ def origin_resources
131
+ @origin_resources ||= resources.select do |resource|
132
+ resource.dig("metadata", "labels", "nine.ch/owned-by") != "nine" &&
133
+ resource.dig("metadata", "name").include?(origin_pattern)
134
+ end
135
+ end
136
+
137
+ def origin_app_name!
138
+ abort "project '#{project}' holds no resources, is its name right?" if resources.empty?
139
+
140
+ origin_resource("Application")&.dig("metadata", "name") ||
141
+ abort("no origin app (name containing '#{origin_pattern}') found in #{project}")
142
+ end
143
+
144
+ def origin_resource(kind)
145
+ origin_resources.find { |resource| resource["kind"] == kind }
146
+ end
147
+
148
+ def resources_of(kind)
149
+ resources.select { |resource| resource["kind"] == kind }
150
+ end
151
+
152
+ def backup_bucket_name
153
+ return nil unless origin_database_name
154
+
155
+ resources_of("DatabaseBackupSchedule")
156
+ .find { |schedule| schedule.dig("spec", "forProvider", "source", "name") == origin_database_name }
157
+ &.dig("status", "atProvider", "targetBucket", "name")
158
+ end
159
+
160
+ def latest_backup_path
161
+ return nil unless origin_backup_bucket
162
+
163
+ resources_of("DatabaseBackup")
164
+ .select { |backup| backup.dig("spec", "forProvider", "bucket", "name") == origin_backup_bucket }
165
+ .select { |backup| backup.dig("status", "atProvider", "state") == "succeeded" }
166
+ .max_by { |backup| backup.dig("status", "atProvider", "end").to_s }
167
+ &.dig("status", "atProvider", "path")
168
+ end
169
+
170
+ def bucket_resource(name)
171
+ name && resources_of("Bucket").find { |bucket| bucket.dig("metadata", "name") == name }
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ class Renuo::Cli::Services::Deploio
6
+ class StartTargetApp < Actor
7
+ include ReviewAppHelper
8
+
9
+ input :project
10
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
11
+ input :branch
12
+
13
+ def call
14
+ puts "starting app #{target_app_name}..."
15
+ system! "nctl update app #{target_app_name} -p #{project} --git-revision #{Shellwords.escape(branch)} " \
16
+ "--skip-repo-access-check --no-pause"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require_relative "create_s3"
5
+
6
+ class Renuo::Cli::Services::Deploio
7
+ class UpdateTargetAppEnv < Actor
8
+ include ReviewAppHelper
9
+
10
+ SENSITIVE_ENV_KEYS = (%w[DATABASE_URL REDIS_URL] + CreateS3::SENSITIVE_ENV_KEYS).freeze
11
+
12
+ input :project
13
+ input :target_app_name, must: satisfy(SafeReviewAppNameContract)
14
+ input :envs
15
+
16
+ def call
17
+ return if env.empty?
18
+
19
+ puts "updating env of app #{target_app_name}..."
20
+ system! ["nctl update app #{target_app_name} -p #{project}",
21
+ env_flag("--env", env.except(*SENSITIVE_ENV_KEYS)),
22
+ env_flag("--sensitive-env", env.slice(*SENSITIVE_ENV_KEYS))].compact.join(" ")
23
+ end
24
+
25
+ private
26
+
27
+ def env
28
+ @env ||= envs.reduce({}, :merge)
29
+ end
30
+
31
+ def env_flag(flag, variables)
32
+ return if variables.empty?
33
+
34
+ "#{flag} #{Shellwords.escape(variables.map { |key, value| "#{key}=#{value}" }.join(";"))}"
35
+ end
36
+ end
37
+ end
@@ -3,7 +3,21 @@
3
3
  require "shellwords"
4
4
 
5
5
  class Renuo::Cli::Services::Deploio
6
+ extend ReviewAppHelper
7
+
6
8
  class << self
9
+ # A named nctl get that misses searches every project of the organization,
10
+ # which project-scoped service accounts are forbidden to do.
11
+ def resource_exists?(type, name, project)
12
+ nctl_resources("nctl get #{type} -p #{project} -o yaml").any? do |resource|
13
+ resource.dig("metadata", "name") == name
14
+ end
15
+ end
16
+
17
+ def fetch_resource(type, name, project)
18
+ nctl_resources("nctl get #{type} #{name} -p #{project} -o yaml").first
19
+ end
20
+
7
21
  def fetch_apps
8
22
  active_org, available_orgs = fetch_whoami_orgs
9
23
 
@@ -33,9 +47,7 @@ class Renuo::Cli::Services::Deploio
33
47
  end
34
48
 
35
49
  def fetch_vms
36
- fetch_vms_cmd = %(nctl get cloudvm -A -o yaml)
37
- stdout, stderr, status = Open3.capture3 fetch_vms_cmd
38
- raise "Error fetching Deploio app list: #{stderr}" unless status.success?
50
+ stdout, = run_nctl_command(%(nctl get cloudvm -A -o yaml))
39
51
 
40
52
  YAML.load_stream(stdout).map do |app|
41
53
  {
@@ -61,7 +73,7 @@ class Renuo::Cli::Services::Deploio
61
73
  end
62
74
 
63
75
  def run_nctl_command(cmd)
64
- stdout, stderr, status = Open3.capture3(cmd)
76
+ stdout, stderr, status = system!(cmd, quiet: true, abort_on_failure: false)
65
77
  raise "Error fetching Deploio app list: #{stderr}" unless status.success?
66
78
 
67
79
  [stdout, stderr, status]
@@ -0,0 +1,44 @@
1
+ version: v1.0
2
+ name: review-app-deploy
3
+
4
+ agent:
5
+ machine:
6
+ type: e1-standard-2
7
+ os_image: ubuntu2404
8
+
9
+ blocks:
10
+ - name: review-app-deploy
11
+ execution_time_limit:
12
+ minutes: 30
13
+ task:
14
+ secrets:
15
+ - name: <%= secret_name %>
16
+ jobs:
17
+ - name: deploying
18
+ commands:
19
+ - gem i -q renuo-cli
20
+ - renuo ci setup
21
+ # pg_restore rejects backups dumped by a newer PostgreSQL, so match or exceed the origin database's version.
22
+ - sudo apt-get install -qq postgresql-common
23
+ - sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
24
+ - sudo apt-get install -qq postgresql-client-18
25
+ - export PATH=/usr/lib/postgresql/18/bin:$PATH
26
+ - renuo review-app:deploy --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH --origin-pattern <%= origin_pattern %>
27
+ # Make sure your review apps have the right environment variables. We don't copy from origin.
28
+ #
29
+ # -| nctl update app $(renuo review-app:name --branch $SEMAPHORE_GIT_WORKING_BRANCH) --project <%= project %> \
30
+ # --env "KEY=value" --sensitive-env "SECRET=$FROM_SEMAPHORE_SECRET" \
31
+ # --build-env "KEY=value" --sensitive-build-env "KEY=value"
32
+ #
33
+ # To find out which env variables a previously existing app uses, inspect its spec:
34
+ #
35
+ # - nctl get app existing-app --project <%= project %> -o yaml
36
+ #
37
+ # The runtime env is at .spec.forProvider.config.env, the build env at .spec.forProvider.buildEnv.
38
+ #
39
+ # If the origin's env variables are safe to reuse on review apps, you can replace this whole
40
+ # block (deploy, env setup, and start) with a single command:
41
+ #
42
+ # - 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
+ - renuo review-app:start --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
44
+ - renuo ci check-deploio-status $(renuo review-app:name --branch $SEMAPHORE_GIT_WORKING_BRANCH) --project <%= project %>
@@ -0,0 +1,21 @@
1
+ version: v1.0
2
+ name: review-app-purge
3
+
4
+ agent:
5
+ machine:
6
+ type: e1-standard-2
7
+ os_image: ubuntu2404
8
+
9
+ blocks:
10
+ - name: review-app-purge
11
+ execution_time_limit:
12
+ minutes: 15
13
+ task:
14
+ secrets:
15
+ - name: <%= secret_name %>
16
+ jobs:
17
+ - name: purging
18
+ commands:
19
+ - gem i -q renuo-cli
20
+ - renuo ci setup
21
+ - renuo review-app:purge --project <%= project %> --branch $SEMAPHORE_GIT_WORKING_BRANCH
@@ -24,6 +24,7 @@ global_job_config:
24
24
  - source .semaphore/bin/cache_restore rails
25
25
  - bundle config set deployment 'true'
26
26
  - bundle config set path 'vendor/bundle'
27
+ - bundle config set clean 'true'
27
28
  - bundle install --retry 3
28
29
 
29
30
  blocks:
@@ -3,7 +3,7 @@
3
3
  # simplecov:disable
4
4
  module Renuo
5
5
  class Cli
6
- VERSION = "4.26.0"
6
+ VERSION = "4.28.0"
7
7
  NAME = "renuo-cli"
8
8
  end
9
9
  end
data/lib/renuo/cli.rb CHANGED
@@ -10,11 +10,21 @@ require "net/http"
10
10
  require "open3"
11
11
  require "psych"
12
12
  require "securerandom"
13
+ require "service_actor"
13
14
  require "tmpdir"
14
15
  require "uri"
15
16
  require "renuo/cli/version"
16
17
 
18
+ module Renuo
19
+ class Cli
20
+ module Commands; end
21
+ module Services; end
22
+ end
23
+ end
24
+
17
25
  Dir[File.expand_path("cli/helpers/*.rb", __dir__)].each { |f| require f }
26
+ require_relative "cli/services/deploio"
27
+ Dir[File.expand_path("cli/services/deploio/*.rb", __dir__)].each { |f| require f }
18
28
 
19
29
  def agree(question)
20
30
  question += " [y/n]" if question