cpflow 6.0.0.rc.0 → 6.0.1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +16 -38
  3. data/.agents/bin/README.md +7 -8
  4. data/.agents/legacy-workflow-policy.yml +11 -0
  5. data/.agents/shaka.md +20 -0
  6. data/.agents/trusted-actions.yml +12 -0
  7. data/.coderabbit.yaml +1 -0
  8. data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
  9. data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
  10. data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
  11. data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
  12. data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +3 -10
  13. data/.github/workflows/cpflow-delete-review-app.yml +4 -11
  14. data/.github/workflows/cpflow-deploy-review-app.yml +19 -14
  15. data/.github/workflows/cpflow-deploy-staging.yml +17 -12
  16. data/.github/workflows/cpflow-promote-staging-to-production.yml +132 -13
  17. data/.github/workflows/rspec.yml +24 -0
  18. data/AGENTS.md +18 -3
  19. data/CHANGELOG.md +26 -3
  20. data/CONTRIBUTING.md +4 -4
  21. data/Gemfile.lock +1 -1
  22. data/README.md +5 -2
  23. data/docs/ai-github-flow-prompt.md +104 -8
  24. data/docs/ci-automation.md +258 -60
  25. data/docs/commands.md +32 -16
  26. data/docs/postgres-review-apps.md +90 -0
  27. data/docs/postgres.md +233 -375
  28. data/docs/secrets-and-env-values.md +42 -0
  29. data/docs/sidebars.ts +1 -0
  30. data/lib/command/ai_github_flow_prompt.rb +10 -13
  31. data/lib/command/ai_github_flow_prompt.txt +103 -0
  32. data/lib/command/apply_template.rb +36 -4
  33. data/lib/command/base.rb +56 -3
  34. data/lib/command/delete.rb +174 -4
  35. data/lib/command/deploy_image.rb +6 -0
  36. data/lib/command/generate.rb +247 -3
  37. data/lib/command/generate_github_actions.rb +15 -17
  38. data/lib/command/setup_app.rb +92 -10
  39. data/lib/command/update_github_actions.rb +107 -43
  40. data/lib/core/config.rb +73 -0
  41. data/lib/core/controlplane.rb +22 -2
  42. data/lib/core/controlplane_api.rb +28 -2
  43. data/lib/core/controlplane_api_direct.rb +17 -0
  44. data/lib/core/disposable_postgres_helpers.rb +167 -0
  45. data/lib/core/doctor_service.rb +7 -0
  46. data/lib/core/github_flow_readiness/checks.rb +2 -1
  47. data/lib/core/github_flow_readiness_service.rb +4 -0
  48. data/lib/core/repo_introspection.rb +112 -2
  49. data/lib/cpflow/version.rb +1 -1
  50. data/lib/generator_templates/.dockerignore +16 -0
  51. data/lib/generator_templates/Dockerfile +9 -5
  52. data/lib/generator_templates/entrypoint.sh +2 -0
  53. data/lib/generator_templates/templates/postgres.yml +9 -0
  54. data/lib/generator_templates_sqlite/controlplane.yml +2 -1
  55. data/lib/generator_templates_sqlite/release_script.sh +1 -1
  56. data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
  57. data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
  58. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
  59. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
  60. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +132 -13
  61. data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
  62. data/lib/github_flow_templates/bin/test-cpflow-github-flow +88 -47
  63. metadata +12 -3
@@ -2,19 +2,28 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "pathname"
5
+ require "optparse"
5
6
 
6
7
  USAGE = <<~USAGE
7
- Usage: bin/pin-cpflow-github-ref [--allow-moving-ref] <control-plane-flow-ref>
8
+ Usage: bin/pin-cpflow-github-ref [--version TAG] [--allow-moving-ref] <control-plane-flow-ref>
8
9
 
9
10
  Use a release tag for normal operation, e.g. v5.0.0.
10
11
  Use a full 40-character commit SHA for temporary unreleased upstream testing.
12
+ --version TAG supplies the reviewed v-prefixed version/tag for SHA comments
13
+ (required for a SHA; no default). Verify the tag/version against upstream first.
14
+ This helper does not resolve tags over the network or verify tag-to-SHA identity.
11
15
  This updates generated reusable-workflow `uses:` refs plus the production
12
16
  workflow's pinned control-plane-flow checkout and setup validation ref.
13
17
  Regenerate from the cpflow gem when templates changed.
14
18
  Use --allow-moving-ref only for short-lived local branch/ref experiments.
19
+ Use -h or --help to show this help.
15
20
  USAGE
16
21
 
17
- ALLOWED_OPTIONS = ["--allow-moving-ref"].freeze
22
+ if ARGV.include?("--help") || ARGV.include?("-h")
23
+ puts USAGE
24
+ exit 0
25
+ end
26
+
18
27
  FULL_COMMIT_SHA = /\A[0-9a-f]{40}\z/i
19
28
  RELEASE_TAG = /\Av\d+\.\d+\.\d+(?:[-.][0-9A-Za-z][0-9A-Za-z.-]*)?\z/
20
29
  PRODUCTION_WORKFLOW_REF = "shakacode/control-plane-flow/.github/workflows/" \
@@ -25,14 +34,19 @@ CPFLOW_CHECKOUT_REF_PATTERN = %r{
25
34
  ^\s+repository:\s+shakacode/control-plane-flow\s*\n
26
35
  (?:(?!^\s*-\s+name:).)*?
27
36
  ^\s+ref:\s+)
28
- [^\s]+
37
+ [^\s\#]+ [\t\ ]* (?:\#[^\n]*)?
29
38
  }mx
30
39
 
31
- options, positional = ARGV.partition { |arg| arg.start_with?("--") }
32
- unknown_options = options - ALLOWED_OPTIONS
33
-
34
- unless unknown_options.empty?
35
- warn "Unknown option(s): #{unknown_options.join(', ')}"
40
+ version = nil
41
+ allow_moving_ref = false
42
+ parser = OptionParser.new do |options|
43
+ options.on("--version TAG") { |value| version = value }
44
+ options.on("--allow-moving-ref") { allow_moving_ref = true }
45
+ end
46
+ begin
47
+ positional = parser.parse(ARGV)
48
+ rescue OptionParser::ParseError => e
49
+ warn e.message
36
50
  warn USAGE
37
51
  exit 1
38
52
  end
@@ -43,13 +57,21 @@ unless positional.length == 1
43
57
  end
44
58
 
45
59
  ref = positional.first
46
- allow_moving_ref = options.include?("--allow-moving-ref")
47
60
 
48
61
  unless ref.match?(%r{\A[0-9A-Za-z._/-]+\z})
49
62
  warn "Ref contains unsupported characters: #{ref.inspect}"
50
63
  exit 1
51
64
  end
52
65
 
66
+ if (version && !version.match?(RELEASE_TAG)) || (ref.match?(FULL_COMMIT_SHA) && !version)
67
+ warn "A SHA pin requires --version TAG with a reviewed v-prefixed version/tag (for example v6.0.0.rc.0)."
68
+ warn USAGE
69
+ exit 1
70
+ end
71
+
72
+ ref = ref.downcase if ref.match?(FULL_COMMIT_SHA)
73
+ annotated_ref = ref.match?(FULL_COMMIT_SHA) ? "#{ref} # #{version}" : ref
74
+
53
75
  unless ref.match?(FULL_COMMIT_SHA) || ref.match?(RELEASE_TAG) || allow_moving_ref
54
76
  warn "Ref must be a full 40-character commit SHA or a v-prefixed release tag: #{ref.inspect}"
55
77
  warn "Use --allow-moving-ref only for a short-lived branch/ref experiment."
@@ -68,11 +90,13 @@ changed = []
68
90
  workflow_paths.each do |path|
69
91
  text = File.read(path)
70
92
  production_setup_ref_pattern =
71
- /(control_plane_flow_ref:\s+#{Regexp.escape(PRODUCTION_WORKFLOW_REF)}@)[^\s]+/
93
+ /(control_plane_flow_ref:\s+#{Regexp.escape(PRODUCTION_WORKFLOW_REF)}@)[^\s#]+[\t ]*(?:#[^\n]*)?/
94
+ workflow_ref_pattern =
95
+ %r{(uses:\s+shakacode/control-plane-flow/\.github/workflows/[^@\s]+@)[^\s#]+[\t ]*(?:#[^\n]*)?}
72
96
  updated = text
73
- .gsub(%r{(uses:\s+shakacode/control-plane-flow/\.github/workflows/[^@\s]+@)[^\s]+}, "\\1#{ref}")
74
- .gsub(production_setup_ref_pattern, "\\1#{ref}")
75
- .gsub(CPFLOW_CHECKOUT_REF_PATTERN, "\\1#{ref}")
97
+ .gsub(workflow_ref_pattern) { "#{Regexp.last_match(1)}#{annotated_ref}" }
98
+ .gsub(production_setup_ref_pattern) { "#{Regexp.last_match(1)}#{annotated_ref}" }
99
+ .gsub(CPFLOW_CHECKOUT_REF_PATTERN) { "#{Regexp.last_match(1)}#{annotated_ref}" }
76
100
 
77
101
  next if updated == text
78
102
 
@@ -29,7 +29,11 @@ documents.each_key do |path|
29
29
  puts "parsed #{path}"
30
30
  end
31
31
 
32
- generated_local_action_pattern = %r{\A\./(?<directory>\.github/actions/cpflow-[a-z0-9]+(?:-[a-z0-9]+)*)\z}
32
+ # Current workflows load cpflow actions from the pinned `.cpflow` checkout, which exists
33
+ # only at run time. Caller-owned `.github/actions/cpflow-*` copies are the cpflow 6.0.0
34
+ # layout; a workflow that still references one needs that copy checked in.
35
+ generated_local_action_pattern =
36
+ %r{\A\./(?<checkout>\.cpflow/)?(?<directory>\.github/actions/cpflow-[a-z0-9]+(?:-[a-z0-9]+)*)\z}
33
37
  local_action_references = []
34
38
  invalid_local_action_references = []
35
39
  visited_container_ids = {}
@@ -43,10 +47,10 @@ walk = lambda do |node, source_path|
43
47
  case node
44
48
  when Hash
45
49
  uses = node["uses"]
46
- if uses.is_a?(String) && uses.start_with?("./.github/actions/cpflow-")
50
+ if uses.is_a?(String) && uses.start_with?("./.github/actions/cpflow-", "./.cpflow/")
47
51
  match = uses.match(generated_local_action_pattern)
48
52
  if match
49
- local_action_references << [source_path, match[:directory]]
53
+ local_action_references << [source_path, match[:directory]] unless match[:checkout]
50
54
  else
51
55
  invalid_local_action_references <<
52
56
  "#{source_path} has invalid generated local action reference #{uses}"
@@ -61,6 +65,24 @@ documents.each { |path, document| walk.call(document, path) }
61
65
 
62
66
  abort invalid_local_action_references.uniq.join("\n") unless invalid_local_action_references.empty?
63
67
 
68
+ # `.cpflow` exists only after a job checks it out, so each job must do that before its first cpflow action.
69
+ missing_checkouts = documents.flat_map do |path, document|
70
+ jobs = document.is_a?(Hash) && document["jobs"].is_a?(Hash) ? document["jobs"] : {}
71
+ jobs.filter_map do |job_name, job|
72
+ steps = job.is_a?(Hash) && job["steps"].is_a?(Array) ? job["steps"].grep(Hash) : []
73
+ first_action = steps.index { |step| step["uses"].to_s.start_with?("./.cpflow/") }
74
+ next unless first_action
75
+
76
+ checked_out = steps.take(first_action).any? do |step|
77
+ step["uses"].to_s.start_with?("actions/checkout@") &&
78
+ step["with"].is_a?(Hash) && step["with"]["path"] == ".cpflow" && !step["with"]["ref"].to_s.strip.empty?
79
+ end
80
+ "#{path}:#{job_name} must check out a pinned ref into .cpflow before its first cpflow action" unless checked_out
81
+ end
82
+ end
83
+
84
+ abort missing_checkouts.join("\n") unless missing_checkouts.empty?
85
+
64
86
  missing_actions = local_action_references.uniq.filter_map do |source_path, action_directory|
65
87
  directory_is_local = begin
66
88
  File.lstat(action_directory).directory?
@@ -79,7 +101,7 @@ missing_actions = local_action_references.uniq.filter_map do |source_path, actio
79
101
  end
80
102
 
81
103
  abort missing_actions.join("\n") unless missing_actions.empty?
82
- puts "all referenced local actions have checked-in descriptors"
104
+ puts "all cpflow action references use the pinned checkout or a checked-in descriptor"
83
105
  RUBY
84
106
 
85
107
  echo "==> check composite action input descriptions"
@@ -142,60 +164,65 @@ Dir[".github/workflows/cpflow-*.yml"].sort.each do |path|
142
164
  end
143
165
 
144
166
  promote_path = ".github/workflows/cpflow-promote-staging-to-production.yml"
145
- promote_doc = YAML.load_file(promote_path, aliases: true)
146
- promote_job = promote_doc.fetch("jobs", {})["promote-to-production"]
167
+ if File.exist?(promote_path)
168
+ promote_doc = YAML.load_file(promote_path, aliases: true)
169
+ promote_job = promote_doc.fetch("jobs", {})["promote-to-production"]
147
170
 
148
- unless promote_job
149
- abort "#{promote_path}:promote-to-production job is missing"
150
- end
171
+ unless promote_job
172
+ abort "#{promote_path}:promote-to-production job is missing"
173
+ end
151
174
 
152
- if promote_job.key?("uses")
153
- abort "#{promote_path}:promote-to-production must run as a normal caller-repo job, not a reusable workflow, so GitHub can expose production environment secrets"
154
- end
175
+ if promote_job.key?("uses")
176
+ abort "#{promote_path}:promote-to-production must run as a normal caller-repo job, not a reusable workflow, so GitHub can expose production environment secrets"
177
+ end
155
178
 
156
- unless promote_job["environment"].to_s == "production"
157
- abort "#{promote_path}:promote-to-production must declare environment: production"
158
- end
179
+ # Older generated workflows use the string form; current ones add a deployment URL.
180
+ promote_environment = promote_job["environment"]
181
+ promote_environment = promote_environment["name"] if promote_environment.is_a?(Hash)
182
+ unless promote_environment.to_s == "production"
183
+ abort "#{promote_path}:promote-to-production must declare environment: production"
184
+ end
159
185
 
160
- checkout_step = Array(promote_job["steps"]).find { |step| step["name"] == "Checkout control-plane-flow actions" }
186
+ checkout_step = Array(promote_job["steps"]).find { |step| step["name"] == "Checkout control-plane-flow actions" }
161
187
 
162
- unless checkout_step
163
- abort "#{promote_path}:promote-to-production must include a Checkout control-plane-flow actions step"
164
- end
188
+ unless checkout_step
189
+ abort "#{promote_path}:promote-to-production must include a Checkout control-plane-flow actions step"
190
+ end
165
191
 
166
- unless checkout_step["uses"] == EXPECTED_CPFLOW_CHECKOUT_ACTION
167
- abort "#{promote_path}:promote-to-production must use #{EXPECTED_CPFLOW_CHECKOUT_ACTION} for the Checkout control-plane-flow actions step"
168
- end
192
+ unless checkout_step["uses"] == EXPECTED_CPFLOW_CHECKOUT_ACTION
193
+ abort "#{promote_path}:promote-to-production must use #{EXPECTED_CPFLOW_CHECKOUT_ACTION} for the Checkout control-plane-flow actions step"
194
+ end
169
195
 
170
- checkout_with = checkout_step.fetch("with", {})
171
- checkout_repository = checkout_with["repository"]
172
- checkout_ref = checkout_with["ref"]
196
+ checkout_with = checkout_step.fetch("with", {})
197
+ checkout_repository = checkout_with["repository"]
198
+ checkout_ref = checkout_with["ref"]
173
199
 
174
- unless checkout_repository == EXPECTED_CPFLOW_CHECKOUT_REPOSITORY
175
- abort "#{promote_path}:promote-to-production must check out #{EXPECTED_CPFLOW_CHECKOUT_REPOSITORY}"
176
- end
200
+ unless checkout_repository == EXPECTED_CPFLOW_CHECKOUT_REPOSITORY
201
+ abort "#{promote_path}:promote-to-production must check out #{EXPECTED_CPFLOW_CHECKOUT_REPOSITORY}"
202
+ end
177
203
 
178
- if checkout_ref.to_s.strip.empty?
179
- abort "#{promote_path}:promote-to-production must pin the Checkout control-plane-flow actions step"
180
- end
204
+ if checkout_ref.to_s.strip.empty?
205
+ abort "#{promote_path}:promote-to-production must pin the Checkout control-plane-flow actions step"
206
+ end
181
207
 
182
- refs[checkout_ref] << "#{promote_path}:promote-to-production"
208
+ refs[checkout_ref] << "#{promote_path}:promote-to-production"
183
209
 
184
- setup_step = Array(promote_job["steps"]).find { |step| step["name"] == "Setup production environment" }
210
+ setup_step = Array(promote_job["steps"]).find { |step| step["name"] == "Setup production environment" }
185
211
 
186
- unless setup_step
187
- abort "#{promote_path}:promote-to-production must include a Setup production environment step"
188
- end
212
+ unless setup_step
213
+ abort "#{promote_path}:promote-to-production must include a Setup production environment step"
214
+ end
189
215
 
190
- setup_ref = setup_step.fetch("with", {})["control_plane_flow_ref"]
191
- setup_match = setup_ref.to_s.match(PROMOTE_WORKFLOW)
216
+ setup_ref = setup_step.fetch("with", {})["control_plane_flow_ref"]
217
+ setup_match = setup_ref.to_s.match(PROMOTE_WORKFLOW)
192
218
 
193
- unless setup_match
194
- abort "#{promote_path}:promote-to-production must pass a pinned production control_plane_flow_ref to setup, " \
195
- "for example #{EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT}"
196
- end
219
+ unless setup_match
220
+ abort "#{promote_path}:promote-to-production must pass a pinned production control_plane_flow_ref to setup, " \
221
+ "for example #{EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT}"
222
+ end
197
223
 
198
- refs[setup_match[1]] << "#{promote_path}:promote-to-production setup"
224
+ refs[setup_match[1]] << "#{promote_path}:promote-to-production setup"
225
+ end
199
226
 
200
227
  if refs.empty?
201
228
  puts "no upstream cpflow reusable workflow refs found"
@@ -210,7 +237,21 @@ end
210
237
  RUBY
211
238
 
212
239
  echo "==> actionlint"
213
- actionlint \
214
- -ignore "SC2129" \
215
- -ignore 'property "workflow_(ref|sha|repository|file_path)" is not defined in object type' \
216
- .github/workflows/cpflow-*.yml
240
+ shopt -s nullglob
241
+ cpflow_workflows=(.github/workflows/cpflow-*.yml)
242
+ if [[ ${#cpflow_workflows[@]} -gt 0 ]]; then
243
+ actionlint \
244
+ -ignore "SC2129" \
245
+ -ignore 'property "workflow_(ref|sha|repository|file_path)" is not defined in object type' \
246
+ "${cpflow_workflows[@]}"
247
+ fi
248
+
249
+ custom_validator="bin/test-cpflow-github-flow-custom"
250
+ if [[ -e "$custom_validator" || -L "$custom_validator" ]]; then
251
+ if [[ ! -f "$custom_validator" || ! -x "$custom_validator" ]]; then
252
+ echo "Custom validator must be an executable file: $custom_validator" >&2
253
+ exit 1
254
+ fi
255
+ echo "==> downstream cpflow GitHub flow checks"
256
+ "$ROOT/$custom_validator" "${cpflow_cmd[@]}"
257
+ fi
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: 6.0.0.rc.0
4
+ version: 6.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -82,6 +82,9 @@ files:
82
82
  - ".agents/bin/setup"
83
83
  - ".agents/bin/test"
84
84
  - ".agents/bin/validate"
85
+ - ".agents/legacy-workflow-policy.yml"
86
+ - ".agents/shaka.md"
87
+ - ".agents/trusted-actions.yml"
85
88
  - ".agents/trusted-github-actors.yml"
86
89
  - ".agents/workflows/address-review.md"
87
90
  - ".agents/workflows/ai-rollout-e2e-test.md"
@@ -92,6 +95,8 @@ files:
92
95
  - ".github/actions/cpflow-delete-control-plane-app/action.yml"
93
96
  - ".github/actions/cpflow-delete-control-plane-app/delete-app.sh"
94
97
  - ".github/actions/cpflow-detect-release-phase/action.yml"
98
+ - ".github/actions/cpflow-preview-promotion/action.yml"
99
+ - ".github/actions/cpflow-preview-promotion/preview-promotion.sh"
95
100
  - ".github/actions/cpflow-resolve-review-config/action.yml"
96
101
  - ".github/actions/cpflow-setup-environment/action.yml"
97
102
  - ".github/actions/cpflow-validate-config/action.yml"
@@ -154,6 +159,7 @@ files:
154
159
  - docs/dns.md
155
160
  - docs/grafana-opentelemetry.md
156
161
  - docs/migrating-heroku-to-control-plane.md
162
+ - docs/postgres-review-apps.md
157
163
  - docs/postgres.md
158
164
  - docs/rds-private-networking.md
159
165
  - docs/redis.md
@@ -178,6 +184,7 @@ files:
178
184
  - examples/circleci.yml
179
185
  - examples/controlplane.yml
180
186
  - lib/command/ai_github_flow_prompt.rb
187
+ - lib/command/ai_github_flow_prompt.txt
181
188
  - lib/command/apply_template.rb
182
189
  - lib/command/base.rb
183
190
  - lib/command/base_sub_command.rb
@@ -225,6 +232,7 @@ files:
225
232
  - lib/core/controlplane.rb
226
233
  - lib/core/controlplane_api.rb
227
234
  - lib/core/controlplane_api_direct.rb
235
+ - lib/core/disposable_postgres_helpers.rb
228
236
  - lib/core/doctor_service.rb
229
237
  - lib/core/github_flow_readiness/checks.rb
230
238
  - lib/core/github_flow_readiness_service.rb
@@ -254,6 +262,7 @@ files:
254
262
  - lib/cpflow.rb
255
263
  - lib/cpflow/version.rb
256
264
  - lib/deprecated_commands.json
265
+ - lib/generator_templates/.dockerignore
257
266
  - lib/generator_templates/Dockerfile
258
267
  - lib/generator_templates/controlplane.yml
259
268
  - lib/generator_templates/entrypoint.sh
@@ -309,7 +318,7 @@ licenses:
309
318
  metadata:
310
319
  rubygems_mfa_required: 'true'
311
320
  post_install_message: |
312
- cpflow 6.0.0.rc.0 installed.
321
+ cpflow 6.0.1 installed.
313
322
  Generated GitHub Actions users: run `cpflow update-github-actions` after upgrading.
314
323
  Docs: https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md#updating-generated-github-actions-after-gem-updates
315
324
  rdoc_options: []
@@ -326,7 +335,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
326
335
  - !ruby/object:Gem::Version
327
336
  version: '0'
328
337
  requirements: []
329
- rubygems_version: 4.0.18
338
+ rubygems_version: 4.0.8
330
339
  specification_version: 4
331
340
  summary: Control Plane Flow
332
341
  test_files: []