cpflow 5.1.1 → 5.2.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/.agents/agent-workflow.yml +15 -0
- data/.agents/bin/README.md +19 -0
- data/.agents/bin/docs +5 -0
- data/.agents/bin/lint +5 -0
- data/.agents/bin/setup +5 -0
- data/.agents/bin/test +5 -0
- data/.agents/bin/validate +5 -0
- data/.agents/trusted-github-actors.yml +32 -0
- data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
- data/.github/workflows/claude-code-review.yml +2 -0
- data/.github/workflows/claude.yml +2 -0
- data/.github/workflows/cpflow-deploy-review-app.yml +16 -1
- data/.github/workflows/rspec-shared.yml +10 -3
- data/.github/workflows/rspec-specific.yml +1 -0
- data/.github/workflows/rspec.yml +58 -1
- data/AGENTS.md +57 -0
- data/CHANGELOG.md +22 -1
- data/CLAUDE.md +3 -0
- data/CONTRIBUTING.md +6 -2
- data/Gemfile.lock +1 -1
- data/README.md +21 -7
- data/docs/ai-github-flow-prompt.md +18 -16
- data/docs/ci-automation.md +160 -13
- data/docs/commands.md +12 -1
- data/docs/grafana-opentelemetry.md +699 -0
- data/docs/secrets-and-env-values.md +29 -2
- data/docs/sidebars.ts +70 -0
- data/docs/telemetry/application-instrumentation.md +161 -0
- data/docs/telemetry/collector.md +297 -0
- data/docs/telemetry/index.md +152 -0
- data/docs/telemetry/pipelines.md +98 -0
- data/docs/telemetry/review-apps.md +55 -0
- data/docs/telemetry/troubleshooting.md +92 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
- data/docs/terraform/overview.md +11 -0
- data/docs/tips.md +458 -29
- data/examples/controlplane.yml +2 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/base.rb +17 -2
- data/lib/command/deploy_image.rb +77 -5
- data/lib/command/promote_app_from_upstream.rb +1 -0
- data/lib/command/ps_wait.rb +2 -10
- data/lib/core/config.rb +94 -0
- data/lib/core/doctor_service.rb +44 -3
- data/lib/core/template_parser.rb +43 -9
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/controlplane.yml +1 -2
- data/lib/github_flow_templates/.github/cpflow-help.md +10 -0
- metadata +21 -2
data/lib/command/deploy_image.rb
CHANGED
|
@@ -3,42 +3,110 @@
|
|
|
3
3
|
require "resolv"
|
|
4
4
|
|
|
5
5
|
module Command
|
|
6
|
-
class DeployImage < Base
|
|
6
|
+
class DeployImage < Base # rubocop:disable Metrics/ClassLength
|
|
7
7
|
NAME = "deploy-image"
|
|
8
8
|
OPTIONS = [
|
|
9
9
|
app_option(required: true),
|
|
10
|
+
workload_option(repeatable: true),
|
|
10
11
|
run_release_phase_option,
|
|
11
12
|
use_digest_image_ref_option
|
|
12
13
|
].freeze
|
|
13
14
|
DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)"
|
|
14
15
|
LONG_DESCRIPTION = <<~DESC
|
|
15
16
|
- Deploys the latest image to app workloads
|
|
17
|
+
- Use `--workload`/`-w` one or more times to deploy only selected app workloads
|
|
18
|
+
- If `deploy_order` is configured and no `--workload` is provided, deploys ordered workload groups one at a time and waits for each group to be ready before continuing
|
|
19
|
+
- Workloads listed in `app_workloads` but omitted from `deploy_order` deploy last as an implicit final group
|
|
16
20
|
- Runs a release script before deploying if `release_script` is specified in the `.controlplane/controlplane.yml` file and `--run-release-phase` is provided
|
|
17
21
|
- The release script is run in the context of `cpflow run` with the latest image
|
|
18
22
|
- If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
|
19
23
|
- If `use_digest_image_ref` is `true` in the `.controlplane/controlplane.yml` file or `--use-digest-image-ref` option is provided, deployed image's reference will include its digest
|
|
20
24
|
- Repairs missing `shared_secret_grants` policy bindings before running a release phase or updating workloads
|
|
21
25
|
DESC
|
|
26
|
+
EXAMPLES = <<~EX
|
|
27
|
+
```sh
|
|
28
|
+
# Deploys the latest image to all app workloads.
|
|
29
|
+
cpflow deploy-image -a $APP_NAME
|
|
30
|
+
|
|
31
|
+
# Deploys only one app workload.
|
|
32
|
+
cpflow deploy-image -a $APP_NAME -w node-renderer
|
|
33
|
+
|
|
34
|
+
# Deploys only selected app workloads.
|
|
35
|
+
cpflow deploy-image -a $APP_NAME -w node-renderer -w sidekiq
|
|
36
|
+
```
|
|
37
|
+
EX
|
|
22
38
|
|
|
23
39
|
def call
|
|
24
40
|
release_script = release_script_to_run
|
|
25
41
|
image = resolve_image_to_deploy
|
|
26
42
|
shared_secret_policy_grant_pairs = resolve_shared_secret_policy_grants
|
|
27
|
-
workload_data_by_name = app_workload_data
|
|
43
|
+
workload_data_by_name = app_workload_data(workload_names_to_deploy)
|
|
28
44
|
|
|
29
45
|
bind_shared_secret_policy_grants(shared_secret_policy_grant_pairs)
|
|
30
46
|
run_release_script(release_script) if release_script
|
|
31
|
-
|
|
47
|
+
deploy_image(image, workload_data_by_name)
|
|
32
48
|
end
|
|
33
49
|
|
|
34
50
|
private
|
|
35
51
|
|
|
36
|
-
def
|
|
37
|
-
config[:app_workloads]
|
|
52
|
+
def workload_names_to_deploy
|
|
53
|
+
app_workloads = config[:app_workloads]
|
|
54
|
+
requested_workloads = requested_workload_names
|
|
55
|
+
return app_workloads if requested_workloads.empty?
|
|
56
|
+
|
|
57
|
+
ensure_workloads_configured!(requested_workloads, app_workloads)
|
|
58
|
+
requested_workloads
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ensure_workloads_configured!(requested_workloads, app_workloads)
|
|
62
|
+
requested_workloads.each do |workload|
|
|
63
|
+
next if app_workloads.include?(workload)
|
|
64
|
+
|
|
65
|
+
raise "Workload '#{workload}' must be listed in app_workloads for app '#{config.app}'."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def app_workload_data(workloads)
|
|
70
|
+
workloads.to_h do |workload|
|
|
38
71
|
[workload, cp.fetch_workload!(workload)]
|
|
39
72
|
end
|
|
40
73
|
end
|
|
41
74
|
|
|
75
|
+
def deploy_image(image, workload_data_by_name)
|
|
76
|
+
deployed_endpoints = {}
|
|
77
|
+
|
|
78
|
+
if deploy_in_order?
|
|
79
|
+
deploy_image_to_ordered_workloads(image, workload_data_by_name, deployed_endpoints)
|
|
80
|
+
else
|
|
81
|
+
deployed_endpoints.merge!(deploy_image_to_workloads(image, workload_data_by_name))
|
|
82
|
+
end
|
|
83
|
+
ensure
|
|
84
|
+
print_deployed_endpoints(deployed_endpoints)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def deploy_in_order?
|
|
88
|
+
requested_workload_names.empty? && config.deploy_order
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def deployment_groups(workload_data_by_name)
|
|
92
|
+
ordered_groups = config.deploy_order
|
|
93
|
+
ordered_workloads = ordered_groups.flatten
|
|
94
|
+
unordered_workloads = workload_data_by_name.keys - ordered_workloads
|
|
95
|
+
|
|
96
|
+
[*ordered_groups, unordered_workloads].reject(&:empty?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def deploy_image_to_ordered_workloads(image, workload_data_by_name, deployed_endpoints)
|
|
100
|
+
deployment_groups(workload_data_by_name).each do |group|
|
|
101
|
+
deployed_endpoints.merge!(deploy_image_to_workloads(image, workload_data_by_name.slice(*group)))
|
|
102
|
+
wait_for_workloads_ready(group)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def requested_workload_names
|
|
107
|
+
@requested_workload_names ||= Array(config.options[:workload]).map(&:to_s).uniq
|
|
108
|
+
end
|
|
109
|
+
|
|
42
110
|
def deploy_image_to_workloads(image, workload_data_by_name) # rubocop:disable Metrics/MethodLength
|
|
43
111
|
deployed_endpoints = {}
|
|
44
112
|
|
|
@@ -57,6 +125,10 @@ module Command
|
|
|
57
125
|
end
|
|
58
126
|
end
|
|
59
127
|
|
|
128
|
+
deployed_endpoints
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def print_deployed_endpoints(deployed_endpoints)
|
|
60
132
|
progress.puts("\nDeployed endpoints:")
|
|
61
133
|
deployed_endpoints.each do |workload, endpoint|
|
|
62
134
|
progress.puts(" - #{workload}: #{endpoint}")
|
|
@@ -14,6 +14,7 @@ module Command
|
|
|
14
14
|
- It performs the following steps:
|
|
15
15
|
- Runs `cpflow copy-image-from-upstream` to copy the latest image from upstream
|
|
16
16
|
- Runs `cpflow deploy-image` to deploy the image
|
|
17
|
+
- Honors `deploy_order` from `.controlplane/controlplane.yml` through `cpflow deploy-image`
|
|
17
18
|
- If `.controlplane/controlplane.yml` includes the `release_script`, `cpflow deploy-image` will use the `--run-release-phase` option
|
|
18
19
|
- If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code
|
|
19
20
|
- If `use_digest_image_ref` is `true` in the `.controlplane/controlplane.yml` file or `--use-digest-image-ref` option is provided, deployed image's reference will include its digest
|
data/lib/command/ps_wait.rb
CHANGED
|
@@ -26,19 +26,11 @@ module Command
|
|
|
26
26
|
```
|
|
27
27
|
EX
|
|
28
28
|
|
|
29
|
-
def call
|
|
29
|
+
def call
|
|
30
30
|
@workloads = [config.options[:workload]] if config.options[:workload]
|
|
31
31
|
@workloads ||= config[:app_workloads] + config[:additional_workloads]
|
|
32
32
|
|
|
33
|
-
@workloads
|
|
34
|
-
if cp.workload_suspended?(workload)
|
|
35
|
-
progress.puts("Workload '#{workload}' is suspended. Skipping...")
|
|
36
|
-
else
|
|
37
|
-
step("Waiting for workload '#{workload}' to be ready", retry_on_failure: true) do
|
|
38
|
-
cp.workload_deployments_ready?(workload, location: config.location, expected_status: true)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
33
|
+
wait_for_workloads_ready(@workloads, reverse: true)
|
|
42
34
|
end
|
|
43
35
|
end
|
|
44
36
|
end
|
data/lib/core/config.rb
CHANGED
|
@@ -166,6 +166,28 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
166
166
|
current&.dig(:use_digest_image_ref) == true
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
def deploy_order
|
|
170
|
+
return nil unless current&.key?(:deploy_order)
|
|
171
|
+
|
|
172
|
+
@deploy_order ||= normalize_deploy_order(
|
|
173
|
+
current.fetch(:deploy_order),
|
|
174
|
+
app_workloads_for_deploy_order!(current, app),
|
|
175
|
+
app
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def validate_deploy_orders!
|
|
180
|
+
apps.each do |app_name, app_options|
|
|
181
|
+
next unless app_options.key?(:deploy_order)
|
|
182
|
+
|
|
183
|
+
normalize_deploy_order(
|
|
184
|
+
app_options.fetch(:deploy_order),
|
|
185
|
+
app_workloads_for_deploy_order!(app_options, app_name),
|
|
186
|
+
app_name
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
169
191
|
private
|
|
170
192
|
|
|
171
193
|
def ensure_current_config!
|
|
@@ -252,6 +274,78 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
252
274
|
end
|
|
253
275
|
end
|
|
254
276
|
|
|
277
|
+
def app_workloads_for_deploy_order!(app_options, app_name)
|
|
278
|
+
app_workloads = app_options[:app_workloads]
|
|
279
|
+
return app_workloads if app_workloads.is_a?(Array)
|
|
280
|
+
|
|
281
|
+
raise "deploy_order for app '#{app_name}' requires app_workloads to be an array."
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def normalize_deploy_order(raw_deploy_order, app_workloads, app_name)
|
|
285
|
+
ensure_deploy_order_array!(raw_deploy_order, app_name)
|
|
286
|
+
context = {
|
|
287
|
+
app_workload_names: app_workloads.map(&:to_s),
|
|
288
|
+
seen_workloads: {},
|
|
289
|
+
app_name: app_name
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
raw_deploy_order.map.with_index do |group, index|
|
|
293
|
+
normalize_deploy_order_group(group, index, context)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ensure_deploy_order_array!(raw_deploy_order, app_name)
|
|
298
|
+
raise "deploy_order for app '#{app_name}' must be an array of workload groups." unless raw_deploy_order.is_a?(Array)
|
|
299
|
+
return unless raw_deploy_order.empty?
|
|
300
|
+
|
|
301
|
+
raise "deploy_order for app '#{app_name}' must include at least one workload group."
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def normalize_deploy_order_group(group, index, context)
|
|
305
|
+
group_number = index + 1
|
|
306
|
+
ensure_deploy_order_group_array!(group, group_number, context.fetch(:app_name))
|
|
307
|
+
|
|
308
|
+
group.map.with_index do |workload, workload_index|
|
|
309
|
+
normalize_deploy_order_workload(workload, group_number, workload_index, context)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def ensure_deploy_order_group_array!(group, group_number, app_name)
|
|
314
|
+
unless group.is_a?(Array)
|
|
315
|
+
raise "deploy_order group ##{group_number} for app '#{app_name}' must be an array of workload names."
|
|
316
|
+
end
|
|
317
|
+
return unless group.empty?
|
|
318
|
+
|
|
319
|
+
raise "deploy_order group ##{group_number} for app '#{app_name}' must include at least one workload."
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def normalize_deploy_order_workload(workload, group_number, workload_index, context)
|
|
323
|
+
app_name = context.fetch(:app_name)
|
|
324
|
+
unless workload.is_a?(String) && !workload.strip.empty?
|
|
325
|
+
raise "deploy_order group ##{group_number} entry ##{workload_index + 1} " \
|
|
326
|
+
"for app '#{app_name}' must be a workload name."
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
workload_name = workload.strip
|
|
330
|
+
ensure_deploy_order_workload_configured!(workload_name, context.fetch(:app_workload_names), app_name)
|
|
331
|
+
ensure_deploy_order_workload_unique!(workload_name, context.fetch(:seen_workloads), app_name)
|
|
332
|
+
workload_name
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def ensure_deploy_order_workload_configured!(workload_name, app_workload_names, app_name)
|
|
336
|
+
return if app_workload_names.include?(workload_name)
|
|
337
|
+
|
|
338
|
+
raise "deploy_order workload '#{workload_name}' must be listed in app_workloads for app '#{app_name}'."
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def ensure_deploy_order_workload_unique!(workload_name, seen_workloads, app_name)
|
|
342
|
+
if seen_workloads[workload_name]
|
|
343
|
+
raise "deploy_order workload '#{workload_name}' must appear only once for app '#{app_name}'."
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
seen_workloads[workload_name] = true
|
|
347
|
+
end
|
|
348
|
+
|
|
255
349
|
def ensure_app!
|
|
256
350
|
return if app
|
|
257
351
|
|
data/lib/core/doctor_service.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class DoctorService
|
|
3
|
+
class DoctorService # rubocop:disable Metrics/ClassLength
|
|
4
4
|
class ValidationError < StandardError; end
|
|
5
5
|
|
|
6
6
|
extend Forwardable
|
|
@@ -36,12 +36,12 @@ class DoctorService
|
|
|
36
36
|
|
|
37
37
|
def validate_config
|
|
38
38
|
check_for_app_names_contained_in_others
|
|
39
|
+
check_deploy_orders
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
def validate_templates
|
|
42
43
|
@template_parser = TemplateParser.new(@command)
|
|
43
|
-
|
|
44
|
-
templates = @template_parser.parse(filenames)
|
|
44
|
+
templates = @template_parser.parse(template_filenames)
|
|
45
45
|
|
|
46
46
|
check_for_duplicate_templates(templates)
|
|
47
47
|
warn_deprecated_template_variables
|
|
@@ -60,6 +60,12 @@ class DoctorService
|
|
|
60
60
|
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
def check_deploy_orders
|
|
64
|
+
config.validate_deploy_orders!
|
|
65
|
+
rescue RuntimeError => e
|
|
66
|
+
raise ValidationError, Shell.color("ERROR: #{e.message}", :red)
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
def find_app_names_contained_in_others # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
64
70
|
app_names = config.apps.keys.map(&:to_s).sort
|
|
65
71
|
app_prefixes = config.apps
|
|
@@ -88,6 +94,41 @@ class DoctorService
|
|
|
88
94
|
raise ValidationError, "#{Shell.color("ERROR: #{message}", :red)}\n#{list}"
|
|
89
95
|
end
|
|
90
96
|
|
|
97
|
+
def template_filenames
|
|
98
|
+
return existing_arg_template_filenames if config.args.any?
|
|
99
|
+
|
|
100
|
+
message = "ERROR: Can't find current config, please specify an app."
|
|
101
|
+
raise ValidationError, Shell.color(message, :red) if config.current.nil?
|
|
102
|
+
|
|
103
|
+
template_names = config.current[:setup_app_templates]
|
|
104
|
+
# Fall back to every template in the directory when setup_app_templates is unconfigured.
|
|
105
|
+
return Dir.glob("#{@template_parser.template_dir}/*.yml") if template_names.nil? || template_names.empty?
|
|
106
|
+
|
|
107
|
+
# When setup_app_templates is configured, validate only that selected subset,
|
|
108
|
+
# including the deprecation scan.
|
|
109
|
+
resolve_template_filenames(template_names)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def existing_arg_template_filenames = resolve_template_filenames(config.args)
|
|
113
|
+
|
|
114
|
+
def resolve_template_filenames(template_names)
|
|
115
|
+
unique_template_names = template_names.uniq
|
|
116
|
+
filenames = unique_template_names.map { |name| @template_parser.template_filename(name) }
|
|
117
|
+
ensure_templates_exist!(unique_template_names, filenames)
|
|
118
|
+
filenames
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def ensure_templates_exist!(template_names, filenames)
|
|
122
|
+
missing_templates = template_names.zip(filenames).reject { |_, filename| File.exist?(filename) }
|
|
123
|
+
return if missing_templates.empty?
|
|
124
|
+
|
|
125
|
+
missing_templates_str = missing_templates.map do |name, filename|
|
|
126
|
+
" - #{name} (#{filename})"
|
|
127
|
+
end.join("\n")
|
|
128
|
+
message = "#{Shell.color('Missing templates:', :red)}\n#{missing_templates_str}"
|
|
129
|
+
raise ValidationError, message
|
|
130
|
+
end
|
|
131
|
+
|
|
91
132
|
def warn_deprecated_template_variables
|
|
92
133
|
deprecated_variables = @template_parser.deprecated_variables
|
|
93
134
|
return if deprecated_variables.empty?
|
data/lib/core/template_parser.rb
CHANGED
|
@@ -37,38 +37,72 @@ class TemplateParser
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
39
|
def replace_variables(yaml_file) # rubocop:disable Metrics/MethodLength
|
|
40
|
+
original_yaml_file = yaml_file
|
|
41
|
+
yaml_file = replace_legacy_variables(yaml_file)
|
|
42
|
+
|
|
40
43
|
yaml_file = yaml_file
|
|
41
44
|
.gsub("{{APP_ORG}}", config.org)
|
|
42
45
|
.gsub("{{APP_NAME}}", config.app)
|
|
43
46
|
.gsub("{{APP_LOCATION}}", config.location)
|
|
44
47
|
.gsub("{{APP_LOCATION_LINK}}", config.location_link)
|
|
45
|
-
.gsub("{{APP_IMAGE}}", cp.latest_image)
|
|
46
|
-
.gsub("{{APP_IMAGE_LINK}}", config.image_link(cp.latest_image))
|
|
47
48
|
.gsub("{{APP_IDENTITY}}", config.identity)
|
|
48
49
|
.gsub("{{APP_IDENTITY_LINK}}", config.identity_link)
|
|
49
50
|
.gsub("{{APP_SECRETS}}", config.secrets)
|
|
50
51
|
.gsub("{{APP_SECRETS_POLICY}}", config.secrets_policy)
|
|
52
|
+
yaml_file = replace_image_variables(yaml_file)
|
|
51
53
|
|
|
52
54
|
config.shared_secret_placeholders.each do |placeholder, secret_name|
|
|
53
55
|
yaml_file = yaml_file.gsub(placeholder, secret_name)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
find_deprecated_variables(
|
|
58
|
+
find_deprecated_variables(original_yaml_file)
|
|
59
|
+
yaml_file
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def replace_image_variables(yaml_file)
|
|
63
|
+
has_image = yaml_file.include?("{{APP_IMAGE}}")
|
|
64
|
+
has_image_link = yaml_file.include?("{{APP_IMAGE_LINK}}")
|
|
65
|
+
return yaml_file unless has_image || has_image_link
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
yaml_file = yaml_file.gsub("{{APP_IMAGE}}", latest_image) if has_image
|
|
68
|
+
yaml_file = yaml_file.gsub("{{APP_IMAGE_LINK}}", config.image_link(latest_image)) if has_image_link
|
|
59
69
|
yaml_file
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Kept for backwards compatibility.
|
|
73
|
+
def replace_legacy_variables(yaml_file)
|
|
74
|
+
yaml_file
|
|
75
|
+
.gsub(deprecated_variable_pattern("APP_ORG"), config.org)
|
|
76
|
+
.gsub(deprecated_variable_pattern("APP_GVC"), config.app)
|
|
77
|
+
.gsub(deprecated_variable_pattern("APP_LOCATION"), config.location)
|
|
78
|
+
.then { |updated_yaml| replace_legacy_image_variable(updated_yaml) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def replace_legacy_image_variable(yaml_file)
|
|
82
|
+
return yaml_file unless deprecated_variable_used?(yaml_file, "APP_IMAGE")
|
|
83
|
+
|
|
84
|
+
yaml_file.gsub(deprecated_variable_pattern("APP_IMAGE"), latest_image)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def latest_image
|
|
88
|
+
# Share one image value across modern and legacy image replacements in this parser instance.
|
|
89
|
+
@latest_image ||= cp.latest_image
|
|
64
90
|
end
|
|
65
91
|
|
|
66
92
|
def find_deprecated_variables(yaml_file)
|
|
67
93
|
new_variables.each do |old_key, new_key|
|
|
68
|
-
@deprecated_variables[old_key] = new_key if
|
|
94
|
+
@deprecated_variables[old_key] = new_key if deprecated_variable_used?(yaml_file, old_key)
|
|
69
95
|
end
|
|
70
96
|
end
|
|
71
97
|
|
|
98
|
+
def deprecated_variable_used?(yaml_file, old_key)
|
|
99
|
+
yaml_file.match?(deprecated_variable_pattern(old_key))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def deprecated_variable_pattern(old_key)
|
|
103
|
+
/(?<!\{\{)\b#{Regexp.escape(old_key)}\b(?!\}\})/
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
def new_variables
|
|
73
107
|
{
|
|
74
108
|
"APP_ORG" => "{{APP_ORG}}",
|
data/lib/cpflow/version.rb
CHANGED
|
@@ -48,10 +48,9 @@ apps:
|
|
|
48
48
|
# - name: database
|
|
49
49
|
# secret_name: __APP_PREFIX__-review-database-secrets
|
|
50
50
|
# policy_name: __APP_PREFIX__-review-database-secrets-policy
|
|
51
|
-
# Uncomment to automatically initialize
|
|
51
|
+
# Uncomment to automatically initialize review-app databases:
|
|
52
52
|
# hooks:
|
|
53
53
|
# post_creation: bundle exec rails db:prepare
|
|
54
|
-
# pre_deletion: bundle exec rails db:drop
|
|
55
54
|
|
|
56
55
|
__APP_PREFIX__-production:
|
|
57
56
|
<<: *production
|
|
@@ -40,6 +40,16 @@ review-app secret dictionaries limited to disposable databases, review-only
|
|
|
40
40
|
renderer credentials, and license values that are acceptable for review-app
|
|
41
41
|
exposure.
|
|
42
42
|
|
|
43
|
+
For public demos, starter staging apps, and long-lived review apps, keep the app
|
|
44
|
+
workload `type: standard` with one warm replica, set its autoscaling metric to
|
|
45
|
+
`disabled`, and enable `capacityAI: true` so Control Plane can right-size CPU and
|
|
46
|
+
memory allocation at that fixed replica count. Shared Postgres and other
|
|
47
|
+
stateful workloads are the usual exceptions and should stay manually sized;
|
|
48
|
+
Capacity AI is for supported stateless app/service workloads. If true idle
|
|
49
|
+
scale-to-zero is explicitly required, create a separate `serverless` workload
|
|
50
|
+
before the first deploy or plan a delete/recreate migration because Control
|
|
51
|
+
Plane will not change an existing `standard` workload to `serverless` in place.
|
|
52
|
+
|
|
43
53
|
Optional overrides exist for forks, clones, and unusual apps:
|
|
44
54
|
|
|
45
55
|
| Name | Notes |
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cpflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Gordon
|
|
@@ -75,7 +75,16 @@ executables:
|
|
|
75
75
|
extensions: []
|
|
76
76
|
extra_rdoc_files: []
|
|
77
77
|
files:
|
|
78
|
+
- ".agents/agent-workflow.yml"
|
|
79
|
+
- ".agents/bin/README.md"
|
|
80
|
+
- ".agents/bin/docs"
|
|
81
|
+
- ".agents/bin/lint"
|
|
82
|
+
- ".agents/bin/setup"
|
|
83
|
+
- ".agents/bin/test"
|
|
84
|
+
- ".agents/bin/validate"
|
|
85
|
+
- ".agents/trusted-github-actors.yml"
|
|
78
86
|
- ".agents/workflows/address-review.md"
|
|
87
|
+
- ".agents/workflows/ai-rollout-e2e-test.md"
|
|
79
88
|
- ".claude/commands/address-review.md"
|
|
80
89
|
- ".claude/commands/update-changelog.md"
|
|
81
90
|
- ".github/actions/cpflow-build-docker-image/action.yml"
|
|
@@ -106,7 +115,9 @@ files:
|
|
|
106
115
|
- ".overcommit.yml"
|
|
107
116
|
- ".rubocop.yml"
|
|
108
117
|
- ".simplecov_spawn.rb"
|
|
118
|
+
- AGENTS.md
|
|
109
119
|
- CHANGELOG.md
|
|
120
|
+
- CLAUDE.md
|
|
110
121
|
- COMM-LICENSE.txt
|
|
111
122
|
- CONTRIBUTING.md
|
|
112
123
|
- Gemfile
|
|
@@ -137,12 +148,20 @@ files:
|
|
|
137
148
|
- docs/ci-automation.md
|
|
138
149
|
- docs/commands.md
|
|
139
150
|
- docs/dns.md
|
|
151
|
+
- docs/grafana-opentelemetry.md
|
|
140
152
|
- docs/migrating-heroku-to-control-plane.md
|
|
141
153
|
- docs/postgres.md
|
|
142
154
|
- docs/rds-private-networking.md
|
|
143
155
|
- docs/redis.md
|
|
144
156
|
- docs/releasing.md
|
|
145
157
|
- docs/secrets-and-env-values.md
|
|
158
|
+
- docs/sidebars.ts
|
|
159
|
+
- docs/telemetry/application-instrumentation.md
|
|
160
|
+
- docs/telemetry/collector.md
|
|
161
|
+
- docs/telemetry/index.md
|
|
162
|
+
- docs/telemetry/pipelines.md
|
|
163
|
+
- docs/telemetry/review-apps.md
|
|
164
|
+
- docs/telemetry/troubleshooting.md
|
|
146
165
|
- docs/terraform/details.md
|
|
147
166
|
- docs/terraform/example/.controlplane/controlplane.yml
|
|
148
167
|
- docs/terraform/example/.controlplane/templates/app.yml
|
|
@@ -284,7 +303,7 @@ licenses:
|
|
|
284
303
|
metadata:
|
|
285
304
|
rubygems_mfa_required: 'true'
|
|
286
305
|
post_install_message: |
|
|
287
|
-
cpflow 5.
|
|
306
|
+
cpflow 5.2.0 installed.
|
|
288
307
|
|
|
289
308
|
If this repository already uses generated cpflow GitHub Actions, update the
|
|
290
309
|
checked-in wrappers so GitHub loads the matching control-plane-flow release tag:
|