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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +15 -0
  3. data/.agents/bin/README.md +19 -0
  4. data/.agents/bin/docs +5 -0
  5. data/.agents/bin/lint +5 -0
  6. data/.agents/bin/setup +5 -0
  7. data/.agents/bin/test +5 -0
  8. data/.agents/bin/validate +5 -0
  9. data/.agents/trusted-github-actors.yml +32 -0
  10. data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
  11. data/.github/workflows/claude-code-review.yml +2 -0
  12. data/.github/workflows/claude.yml +2 -0
  13. data/.github/workflows/cpflow-deploy-review-app.yml +16 -1
  14. data/.github/workflows/rspec-shared.yml +10 -3
  15. data/.github/workflows/rspec-specific.yml +1 -0
  16. data/.github/workflows/rspec.yml +58 -1
  17. data/AGENTS.md +57 -0
  18. data/CHANGELOG.md +22 -1
  19. data/CLAUDE.md +3 -0
  20. data/CONTRIBUTING.md +6 -2
  21. data/Gemfile.lock +1 -1
  22. data/README.md +21 -7
  23. data/docs/ai-github-flow-prompt.md +18 -16
  24. data/docs/ci-automation.md +160 -13
  25. data/docs/commands.md +12 -1
  26. data/docs/grafana-opentelemetry.md +699 -0
  27. data/docs/secrets-and-env-values.md +29 -2
  28. data/docs/sidebars.ts +70 -0
  29. data/docs/telemetry/application-instrumentation.md +161 -0
  30. data/docs/telemetry/collector.md +297 -0
  31. data/docs/telemetry/index.md +152 -0
  32. data/docs/telemetry/pipelines.md +98 -0
  33. data/docs/telemetry/review-apps.md +55 -0
  34. data/docs/telemetry/troubleshooting.md +92 -0
  35. data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
  36. data/docs/terraform/overview.md +11 -0
  37. data/docs/tips.md +458 -29
  38. data/examples/controlplane.yml +2 -0
  39. data/lib/command/ai_github_flow_prompt.rb +2 -2
  40. data/lib/command/base.rb +17 -2
  41. data/lib/command/deploy_image.rb +77 -5
  42. data/lib/command/promote_app_from_upstream.rb +1 -0
  43. data/lib/command/ps_wait.rb +2 -10
  44. data/lib/core/config.rb +94 -0
  45. data/lib/core/doctor_service.rb +44 -3
  46. data/lib/core/template_parser.rb +43 -9
  47. data/lib/cpflow/version.rb +1 -1
  48. data/lib/generator_templates/controlplane.yml +1 -2
  49. data/lib/github_flow_templates/.github/cpflow-help.md +10 -0
  50. metadata +21 -2
@@ -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
- deploy_image_to_workloads(image, workload_data_by_name)
47
+ deploy_image(image, workload_data_by_name)
32
48
  end
33
49
 
34
50
  private
35
51
 
36
- def app_workload_data
37
- config[:app_workloads].to_h do |workload|
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
@@ -26,19 +26,11 @@ module Command
26
26
  ```
27
27
  EX
28
28
 
29
- def call # rubocop:disable Metrics/MethodLength
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.reverse_each do |workload|
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
 
@@ -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
- filenames = Dir.glob("#{@template_parser.template_dir}/*.yml")
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?
@@ -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(yaml_file)
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
- # Kept for backwards compatibility
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
- .gsub("APP_ORG", config.org)
61
- .gsub("APP_GVC", config.app)
62
- .gsub("APP_LOCATION", config.location)
63
- .gsub("APP_IMAGE", cp.latest_image)
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 yaml_file.include?(old_key)
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}}",
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cpflow
4
- VERSION = "5.1.1"
4
+ VERSION = "5.2.0"
5
5
  MIN_CPLN_VERSION = "3.1.0"
6
6
  end
@@ -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 and tear down review-app databases:
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.1.1
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.1.1 installed.
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: