cpflow 5.2.0 → 6.0.0.rc.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +26 -2
  3. data/.agents/bin/README.md +16 -4
  4. data/.agents/bin/docs +1 -1
  5. data/.agents/bin/lint +1 -1
  6. data/.agents/bin/setup +1 -1
  7. data/.agents/bin/test +1 -1
  8. data/.agents/bin/validate +2 -1
  9. data/.coderabbit.yaml +13 -0
  10. data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
  11. data/.github/actions/cpflow-setup-environment/action.yml +9 -7
  12. data/.github/actions/cpflow-wait-for-health/action.yml +87 -15
  13. data/.github/dependabot.yml +10 -0
  14. data/.github/pull_request_template.md +18 -0
  15. data/.github/workflows/check_cpln_links.yml +6 -1
  16. data/.github/workflows/claude-code-review.yml +5 -2
  17. data/.github/workflows/claude.yml +97 -4
  18. data/.github/workflows/coderabbit-lifecycle-review.yml +29 -0
  19. data/.github/workflows/command_docs.yml +7 -2
  20. data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +12 -5
  21. data/.github/workflows/cpflow-delete-review-app.yml +638 -43
  22. data/.github/workflows/cpflow-deploy-review-app.yml +665 -36
  23. data/.github/workflows/cpflow-deploy-staging.yml +16 -10
  24. data/.github/workflows/cpflow-help-command.yml +3 -3
  25. data/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
  26. data/.github/workflows/cpflow-review-app-help.yml +6 -14
  27. data/.github/workflows/rspec-shared.yml +34 -26
  28. data/.github/workflows/rspec-specific.yml +10 -2
  29. data/.github/workflows/rspec.yml +71 -4
  30. data/.github/workflows/rubocop.yml +9 -2
  31. data/.github/workflows/trigger-docs-site.yml +2 -0
  32. data/.rubocop.yml +6 -1
  33. data/AGENTS.md +6 -49
  34. data/CHANGELOG.md +56 -1
  35. data/CONTRIBUTING.md +48 -3
  36. data/Gemfile.lock +1 -1
  37. data/README.md +6 -1
  38. data/cpflow.gemspec +3 -15
  39. data/docs/ai-github-flow-prompt.md +2 -2
  40. data/docs/ci-automation.md +216 -99
  41. data/docs/commands.md +36 -9
  42. data/docs/rds-private-networking.md +12 -12
  43. data/docs/releasing.md +15 -4
  44. data/docs/secrets-and-env-values.md +8 -0
  45. data/docs/tips.md +18 -0
  46. data/examples/controlplane.yml +5 -0
  47. data/lib/command/ai_github_flow_prompt.rb +1 -1
  48. data/lib/command/apply_template.rb +104 -2
  49. data/lib/command/base.rb +52 -3
  50. data/lib/command/cleanup_stale_apps.rb +28 -4
  51. data/lib/command/copy_image_from_upstream.rb +3 -0
  52. data/lib/command/deploy_image.rb +54 -3
  53. data/lib/command/generate_github_actions.rb +36 -12
  54. data/lib/command/run.rb +357 -44
  55. data/lib/command/setup_app.rb +10 -5
  56. data/lib/command/update_github_actions.rb +7 -6
  57. data/lib/core/controlplane.rb +179 -79
  58. data/lib/core/controlplane_api.rb +8 -0
  59. data/lib/core/controlplane_api_direct.rb +257 -63
  60. data/lib/core/shell.rb +39 -7
  61. data/lib/core/timed_command.rb +111 -0
  62. data/lib/cpflow/version.rb +1 -1
  63. data/lib/cpflow.rb +1 -1
  64. data/lib/github_flow_templates/.github/cpflow-help.md +24 -10
  65. data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +10 -0
  66. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +9 -0
  67. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +7 -7
  68. data/lib/github_flow_templates/bin/test-cpflow-github-flow +63 -3
  69. data/lib/patches/hash.rb +2 -2
  70. data/rakelib/create_release.rake +14 -14
  71. data/script/check_shell_scripts +66 -0
  72. metadata +12 -18
@@ -13,15 +13,24 @@ on:
13
13
  description: Pull request number to deploy
14
14
  required: true
15
15
  type: number
16
+ reconcile_intent_run_id:
17
+ description: Authenticated internal handoff; manual values are rejected
18
+ required: false
19
+ type: string
16
20
 
17
21
  permissions:
22
+ actions: write
18
23
  contents: read
19
24
  deployments: write
20
25
  issues: write
21
26
  pull-requests: write
22
27
 
23
28
  jobs:
29
+ # The reusable job exposes `image_built`; downstream jobs can read
30
+ # `needs.deploy.outputs.image_built`. A value of `false` means this check did not validate the Docker image.
24
31
  deploy:
32
+ # author_association is a cheap caller-side cost filter. The reusable workflow
33
+ # still checks the commenter's current repository permission before privileged work.
25
34
  if: |
26
35
  (github.event_name == 'pull_request' &&
27
36
  github.event.pull_request.head.repo.full_name == github.repository) ||
@@ -61,12 +61,12 @@ jobs:
61
61
 
62
62
  steps:
63
63
  - name: Checkout repository
64
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
64
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
65
65
  with:
66
66
  persist-credentials: false
67
67
 
68
68
  - name: Checkout control-plane-flow actions
69
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
69
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
70
70
  with:
71
71
  repository: shakacode/control-plane-flow
72
72
  ref: __CPFLOW_GITHUB_ACTIONS_REF__
@@ -90,7 +90,7 @@ jobs:
90
90
  fi
91
91
 
92
92
  - name: Validate required secrets and variables
93
- uses: ./.cpflow/.github/actions/cpflow-validate-config
93
+ uses: ./.github/actions/cpflow-validate-config
94
94
  # Pass secrets via env so the composite action checks indirect shell
95
95
  # variables instead of interpolating secret values into a run script.
96
96
  env:
@@ -171,7 +171,7 @@ jobs:
171
171
  } >> "$GITHUB_OUTPUT"
172
172
 
173
173
  - name: Setup production environment
174
- uses: ./.cpflow/.github/actions/cpflow-setup-environment
174
+ uses: ./.github/actions/cpflow-setup-environment
175
175
  with:
176
176
  token: ${{ secrets.CPLN_TOKEN_PRODUCTION }}
177
177
  org: ${{ steps.cpln-orgs.outputs.production }}
@@ -237,7 +237,7 @@ jobs:
237
237
 
238
238
  - name: Detect release phase support
239
239
  id: release-phase
240
- uses: ./.cpflow/.github/actions/cpflow-detect-release-phase
240
+ uses: ./.github/actions/cpflow-detect-release-phase
241
241
  with:
242
242
  app_name: ${{ vars.PRODUCTION_APP_NAME }}
243
243
 
@@ -441,7 +441,7 @@ jobs:
441
441
  echo "image=${staging_image}" >> "$GITHUB_OUTPUT"
442
442
 
443
443
  - name: Set up Docker Buildx
444
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
444
+ uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
445
445
 
446
446
  - name: Copy image from staging
447
447
  id: copy-image
@@ -589,7 +589,7 @@ jobs:
589
589
 
590
590
  - name: Wait for deployment health
591
591
  id: health-check
592
- uses: ./.cpflow/.github/actions/cpflow-wait-for-health
592
+ uses: ./.github/actions/cpflow-wait-for-health
593
593
  with:
594
594
  workload_name: ${{ steps.workloads.outputs.primary }}
595
595
  app_name: ${{ vars.PRODUCTION_APP_NAME }}
@@ -16,10 +16,70 @@ echo "==> parse generated GitHub Actions YAML"
16
16
  ruby <<'RUBY'
17
17
  require "yaml"
18
18
 
19
- Dir[".github/actions/**/action.yml", ".github/workflows/*.yml"].sort.each do |path|
20
- YAML.load_file(path, aliases: true)
19
+ documents = Dir[
20
+ ".github/actions/**/action.yml",
21
+ ".github/actions/**/action.yaml",
22
+ ".github/workflows/*.yml",
23
+ ".github/workflows/*.yaml"
24
+ ].sort.to_h do |path|
25
+ [path, YAML.load_file(path, aliases: true)]
26
+ end
27
+
28
+ documents.each_key do |path|
21
29
  puts "parsed #{path}"
22
30
  end
31
+
32
+ generated_local_action_pattern = %r{\A\./(?<directory>\.github/actions/cpflow-[a-z0-9]+(?:-[a-z0-9]+)*)\z}
33
+ local_action_references = []
34
+ invalid_local_action_references = []
35
+ visited_container_ids = {}
36
+ walk = lambda do |node, source_path|
37
+ if node.is_a?(Hash) || node.is_a?(Array)
38
+ next if visited_container_ids.key?(node.object_id)
39
+
40
+ visited_container_ids[node.object_id] = true
41
+ end
42
+
43
+ case node
44
+ when Hash
45
+ uses = node["uses"]
46
+ if uses.is_a?(String) && uses.start_with?("./.github/actions/cpflow-")
47
+ match = uses.match(generated_local_action_pattern)
48
+ if match
49
+ local_action_references << [source_path, match[:directory]]
50
+ else
51
+ invalid_local_action_references <<
52
+ "#{source_path} has invalid generated local action reference #{uses}"
53
+ end
54
+ end
55
+ node.each_value { |value| walk.call(value, source_path) }
56
+ when Array
57
+ node.each { |value| walk.call(value, source_path) }
58
+ end
59
+ end
60
+ documents.each { |path, document| walk.call(document, path) }
61
+
62
+ abort invalid_local_action_references.uniq.join("\n") unless invalid_local_action_references.empty?
63
+
64
+ missing_actions = local_action_references.uniq.filter_map do |source_path, action_directory|
65
+ directory_is_local = begin
66
+ File.lstat(action_directory).directory?
67
+ rescue Errno::ENOENT
68
+ false
69
+ end
70
+ descriptor_is_local = %w[action.yml action.yaml].any? do |filename|
71
+ begin
72
+ File.lstat(File.join(action_directory, filename)).file?
73
+ rescue Errno::ENOENT
74
+ false
75
+ end
76
+ end
77
+
78
+ "#{source_path} references missing local action #{action_directory}" unless directory_is_local && descriptor_is_local
79
+ end
80
+
81
+ abort missing_actions.join("\n") unless missing_actions.empty?
82
+ puts "all referenced local actions have checked-in descriptors"
23
83
  RUBY
24
84
 
25
85
  echo "==> check composite action input descriptions"
@@ -45,7 +105,7 @@ require "yaml"
45
105
  CONTROL_PLANE_FLOW_WORKFLOW = %r{\Ashakacode/control-plane-flow/\.github/workflows/[^@\s]+@([^\s]+)\z}
46
106
  PROMOTE_WORKFLOW = %r{\Ashakacode/control-plane-flow/\.github/workflows/cpflow-promote-staging-to-production\.yml@([^\s]+)\z}
47
107
  EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT = "shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@vX.Y.Z"
48
- EXPECTED_CPFLOW_CHECKOUT_ACTION = "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd"
108
+ EXPECTED_CPFLOW_CHECKOUT_ACTION = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"
49
109
  EXPECTED_CPFLOW_CHECKOUT_REPOSITORY = "shakacode/control-plane-flow"
50
110
 
51
111
  refs = Hash.new { |hash, key| hash[key] = [] }
data/lib/patches/hash.rb CHANGED
@@ -27,8 +27,8 @@ class Hash
27
27
  private
28
28
 
29
29
  # Copied from Rails
30
- def deep_transform_keys(&block)
31
- deep_transform_keys_in_object(self, &block)
30
+ def deep_transform_keys(&)
31
+ deep_transform_keys_in_object(self, &)
32
32
  end
33
33
 
34
34
  # Copied from Rails
@@ -22,8 +22,9 @@ desc("Releases the cpflow Ruby gem.
22
22
  3. Enter the RubyGems OTP when prompted.
23
23
 
24
24
  With no version argument, the task reads the latest versioned CHANGELOG.md
25
- header and uses it when it is newer than the current gem version. Otherwise,
26
- it falls back to a patch bump.
25
+ header and uses it when it is newer than or equal to the current gem version.
26
+ It falls back to a patch bump only when the current version is stable and the
27
+ changelog does not name the current or a newer version.
27
28
 
28
29
  1st argument: Version (optional). Supported values:
29
30
  patch, minor, major, 4.2.0, or 4.2.0.rc.1
@@ -205,10 +206,15 @@ module Release
205
206
 
206
207
  version = parse_gem_version_components(current_gem_version)
207
208
 
209
+ if version[:prerelease_type] && version_input.to_s.strip.casecmp?("patch")
210
+ abort <<~ERROR
211
+ Automatic patch bumps are not allowed from prerelease version #{current_gem_version}.
212
+ Pass an explicit version instead so a retry cannot accidentally promote a prerelease to stable.
213
+ ERROR
214
+ end
215
+
208
216
  case version_input.to_s.strip.downcase
209
217
  when "patch"
210
- return "#{version[:major]}.#{version[:minor]}.#{version[:patch]}" if version[:prerelease_type]
211
-
212
218
  "#{version[:major]}.#{version[:minor]}.#{version[:patch] + 1}"
213
219
  when "minor"
214
220
  "#{version[:major]}.#{version[:minor] + 1}.0"
@@ -269,10 +275,8 @@ module Release
269
275
  return changelog_version
270
276
  end
271
277
 
272
- if changelog_version &&
273
- Gem::Version.new(changelog_version) == Gem::Version.new(current_version) &&
274
- !version_tagged?(gem_root, changelog_version)
275
- puts "Found untagged CHANGELOG.md version: #{changelog_version} (current: #{current_version})"
278
+ if changelog_version && Gem::Version.new(changelog_version) == Gem::Version.new(current_version)
279
+ puts "Found current CHANGELOG.md version: #{changelog_version}"
276
280
  return changelog_version
277
281
  end
278
282
 
@@ -308,10 +312,6 @@ module Release
308
312
  tags_output.lines.map(&:strip).filter_map { |tag| parse_release_tag_to_gem_version(tag) }.uniq
309
313
  end
310
314
 
311
- def version_tagged?(gem_root, version)
312
- tagged_release_gem_versions(gem_root, fetch_tags: true).include?(version)
313
- end
314
-
315
315
  def version_bump_type(previous_stable_gem_version:, target_gem_version:)
316
316
  previous = parse_gem_version_components(previous_stable_gem_version)
317
317
  target = parse_gem_version_components(target_gem_version)
@@ -479,8 +479,8 @@ module Release
479
479
  match[:repo]
480
480
  end
481
481
 
482
- def capture_gh_output(*args)
483
- Open3.capture2e("gh", *args)
482
+ def capture_gh_output(*)
483
+ Open3.capture2e("gh", *)
484
484
  rescue Errno::ENOENT
485
485
  abort "GitHub CLI (`gh`) is not installed. Install it from https://cli.github.com/ and retry."
486
486
  end
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
5
+ cd "$repo_root"
6
+
7
+ if ! command -v shellcheck >/dev/null 2>&1; then
8
+ echo "ShellCheck is required; install it and ensure shellcheck is on PATH." >&2
9
+ exit 127
10
+ fi
11
+
12
+ # Add, rename, or remove extensionless shell entrypoints here. See .agents/bin/README.md.
13
+ extensionless_shell_files=(
14
+ .agents/bin/docs
15
+ .agents/bin/lint
16
+ .agents/bin/setup
17
+ .agents/bin/test
18
+ .agents/bin/validate
19
+ lib/github_flow_templates/bin/test-cpflow-github-flow
20
+ script/check_command_docs
21
+ script/check_cpln_links
22
+ script/check_shell_scripts
23
+ script/precommit/check_command_docs
24
+ script/precommit/check_cpln_links
25
+ script/precommit/check_trailing_newlines
26
+ script/precommit/get_changed_files
27
+ script/precommit/ruby_autofix
28
+ script/precommit/ruby_lint
29
+ spec/dummy/bin/dev
30
+ )
31
+
32
+ # A file preserves NUL-separated names while letting us check Git's exit status.
33
+ inventory_file=$(mktemp)
34
+ trap 'rm -f -- "$inventory_file"' EXIT
35
+ if ! git ls-files -z -- '*.sh' '*.bash' "${extensionless_shell_files[@]}" >"$inventory_file"; then
36
+ echo "Unable to read tracked shell inventory." >&2
37
+ exit 1
38
+ fi
39
+
40
+ shell_files=()
41
+ while IFS= read -r -d '' file; do
42
+ if [ ! -r "$file" ]; then
43
+ echo "Tracked shell script is not readable: $file" >&2
44
+ exit 1
45
+ fi
46
+ shell_files+=("$file")
47
+ done < "$inventory_file"
48
+
49
+ if [ "${#shell_files[@]}" -eq 0 ]; then
50
+ echo "No tracked shell scripts found." >&2
51
+ exit 1
52
+ fi
53
+
54
+ for file in "${extensionless_shell_files[@]}"; do
55
+ if [ ! -f "$file" ] || [ ! -r "$file" ]; then
56
+ echo "Declared shell entrypoint is not a readable file: $file" >&2
57
+ exit 1
58
+ fi
59
+ done
60
+
61
+ if ! git ls-files --error-unmatch -- "${extensionless_shell_files[@]}" >/dev/null; then
62
+ echo "Declared extensionless shell scripts must be tracked; update the inventory for renamed or removed files." >&2
63
+ exit 1
64
+ fi
65
+
66
+ shellcheck -- "${shell_files[@]}"
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.2.0
4
+ version: 6.0.0.rc.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -87,6 +87,7 @@ files:
87
87
  - ".agents/workflows/ai-rollout-e2e-test.md"
88
88
  - ".claude/commands/address-review.md"
89
89
  - ".claude/commands/update-changelog.md"
90
+ - ".coderabbit.yaml"
90
91
  - ".github/actions/cpflow-build-docker-image/action.yml"
91
92
  - ".github/actions/cpflow-delete-control-plane-app/action.yml"
92
93
  - ".github/actions/cpflow-delete-control-plane-app/delete-app.sh"
@@ -95,9 +96,12 @@ files:
95
96
  - ".github/actions/cpflow-setup-environment/action.yml"
96
97
  - ".github/actions/cpflow-validate-config/action.yml"
97
98
  - ".github/actions/cpflow-wait-for-health/action.yml"
99
+ - ".github/dependabot.yml"
100
+ - ".github/pull_request_template.md"
98
101
  - ".github/workflows/check_cpln_links.yml"
99
102
  - ".github/workflows/claude-code-review.yml"
100
103
  - ".github/workflows/claude.yml"
104
+ - ".github/workflows/coderabbit-lifecycle-review.yml"
101
105
  - ".github/workflows/command_docs.yml"
102
106
  - ".github/workflows/cpflow-cleanup-stale-review-apps.yml"
103
107
  - ".github/workflows/cpflow-delete-review-app.yml"
@@ -246,6 +250,7 @@ files:
246
250
  - lib/core/terraform_config/workload/main.tf
247
251
  - lib/core/terraform_config/workload/required_providers.tf
248
252
  - lib/core/terraform_config/workload/variables.tf
253
+ - lib/core/timed_command.rb
249
254
  - lib/cpflow.rb
250
255
  - lib/cpflow/version.rb
251
256
  - lib/deprecated_commands.json
@@ -280,6 +285,7 @@ files:
280
285
  - script/add_command
281
286
  - script/check_command_docs
282
287
  - script/check_cpln_links
288
+ - script/check_shell_scripts
283
289
  - script/precommit/check_command_docs
284
290
  - script/precommit/check_cpln_links
285
291
  - script/precommit/check_trailing_newlines
@@ -303,21 +309,9 @@ licenses:
303
309
  metadata:
304
310
  rubygems_mfa_required: 'true'
305
311
  post_install_message: |
306
- cpflow 5.2.0 installed.
307
-
308
- If this repository already uses generated cpflow GitHub Actions, update the
309
- checked-in wrappers so GitHub loads the matching control-plane-flow release tag:
310
-
311
- cpflow update-github-actions
312
- bin/test-cpflow-github-flow
313
-
314
- If you run cpflow through Bundler:
315
-
316
- bundle exec cpflow update-github-actions
317
- bin/test-cpflow-github-flow bundle exec cpflow
318
-
319
- New repository? Run `cpflow generate-github-actions` first to create the
320
- wrappers (and the `bin/test-cpflow-github-flow` script referenced above).
312
+ cpflow 6.0.0.rc.0 installed.
313
+ Generated GitHub Actions users: run `cpflow update-github-actions` after upgrading.
314
+ Docs: https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md#updating-generated-github-actions-after-gem-updates
321
315
  rdoc_options: []
322
316
  require_paths:
323
317
  - lib
@@ -325,14 +319,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
325
319
  requirements:
326
320
  - - ">="
327
321
  - !ruby/object:Gem::Version
328
- version: 3.0.0
322
+ version: '3.2'
329
323
  required_rubygems_version: !ruby/object:Gem::Requirement
330
324
  requirements:
331
325
  - - ">="
332
326
  - !ruby/object:Gem::Version
333
327
  version: '0'
334
328
  requirements: []
335
- rubygems_version: 4.0.8
329
+ rubygems_version: 4.0.18
336
330
  specification_version: 4
337
331
  summary: Control Plane Flow
338
332
  test_files: []