cpflow 5.3.0 → 6.0.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 +16 -25
- data/.agents/bin/README.md +19 -9
- data/.agents/bin/docs +1 -1
- data/.agents/bin/lint +1 -1
- data/.agents/bin/setup +1 -1
- data/.agents/bin/test +1 -1
- data/.agents/bin/validate +2 -1
- data/.agents/legacy-workflow-policy.yml +11 -0
- data/.agents/shaka.md +20 -0
- data/.agents/trusted-actions.yml +12 -0
- data/.coderabbit.yaml +14 -0
- data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
- data/.github/actions/cpflow-delete-control-plane-app/action.yml +2 -1
- data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
- data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
- data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
- data/.github/actions/cpflow-setup-environment/action.yml +8 -6
- data/.github/dependabot.yml +10 -0
- data/.github/workflows/check_cpln_links.yml +6 -1
- data/.github/workflows/claude-code-review.yml +5 -2
- data/.github/workflows/claude.yml +6 -4
- data/.github/workflows/coderabbit-lifecycle-review.yml +29 -0
- data/.github/workflows/command_docs.yml +7 -2
- data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +12 -5
- data/.github/workflows/cpflow-delete-review-app.yml +18 -11
- data/.github/workflows/cpflow-deploy-review-app.yml +38 -17
- data/.github/workflows/cpflow-deploy-staging.yml +28 -11
- data/.github/workflows/cpflow-help-command.yml +3 -3
- data/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
- data/.github/workflows/cpflow-review-app-help.yml +1 -1
- data/.github/workflows/rspec-shared.yml +34 -26
- data/.github/workflows/rspec-specific.yml +10 -2
- data/.github/workflows/rspec.yml +95 -4
- data/.github/workflows/rubocop.yml +9 -2
- data/.github/workflows/trigger-docs-site.yml +2 -0
- data/.rubocop.yml +6 -1
- data/AGENTS.md +18 -3
- data/CHANGELOG.md +39 -1
- data/CONTRIBUTING.md +39 -2
- data/Gemfile.lock +1 -1
- data/README.md +10 -2
- data/cpflow.gemspec +3 -15
- data/docs/ai-github-flow-prompt.md +3 -3
- data/docs/ci-automation.md +284 -68
- data/docs/commands.md +38 -15
- data/docs/postgres-review-apps.md +90 -0
- data/docs/postgres.md +233 -375
- data/docs/rds-private-networking.md +12 -12
- data/docs/releasing.md +15 -4
- data/docs/secrets-and-env-values.md +42 -0
- data/docs/sidebars.ts +1 -0
- data/examples/controlplane.yml +5 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/apply_template.rb +36 -4
- data/lib/command/base.rb +56 -3
- data/lib/command/cleanup_stale_apps.rb +28 -4
- data/lib/command/copy_image_from_upstream.rb +3 -0
- data/lib/command/delete.rb +174 -4
- data/lib/command/deploy_image.rb +44 -1
- data/lib/command/generate.rb +247 -3
- data/lib/command/generate_github_actions.rb +43 -13
- data/lib/command/run.rb +224 -34
- data/lib/command/setup_app.rb +92 -10
- data/lib/command/update_github_actions.rb +82 -43
- data/lib/core/config.rb +73 -0
- data/lib/core/controlplane.rb +167 -80
- data/lib/core/controlplane_api.rb +28 -2
- data/lib/core/controlplane_api_direct.rb +17 -0
- data/lib/core/disposable_postgres_helpers.rb +167 -0
- data/lib/core/doctor_service.rb +7 -0
- data/lib/core/github_flow_readiness/checks.rb +2 -1
- data/lib/core/github_flow_readiness_service.rb +4 -0
- data/lib/core/repo_introspection.rb +112 -2
- data/lib/core/shell.rb +35 -10
- data/lib/core/timed_command.rb +111 -0
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +1 -1
- data/lib/generator_templates/.dockerignore +16 -0
- data/lib/generator_templates/Dockerfile +9 -5
- data/lib/generator_templates/entrypoint.sh +2 -0
- data/lib/generator_templates/templates/postgres.yml +9 -0
- data/lib/generator_templates_sqlite/controlplane.yml +2 -1
- data/lib/generator_templates_sqlite/release_script.sh +1 -1
- data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
- data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +135 -16
- data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
- data/lib/github_flow_templates/bin/test-cpflow-github-flow +125 -46
- data/lib/patches/hash.rb +2 -2
- data/rakelib/create_release.rake +14 -14
- data/script/check_shell_scripts +66 -0
- metadata +18 -17
|
@@ -5,32 +5,49 @@ require "yaml"
|
|
|
5
5
|
require_relative "staging_branch_validation"
|
|
6
6
|
|
|
7
7
|
module Command
|
|
8
|
-
class UpdateGithubActions < Base
|
|
8
|
+
class UpdateGithubActions < Base # rubocop:disable Metrics/ClassLength
|
|
9
9
|
include StagingBranchValidation
|
|
10
10
|
|
|
11
11
|
NAME = "update-github-actions"
|
|
12
|
-
OPTIONS = [staging_branch_option
|
|
13
|
-
|
|
12
|
+
OPTIONS = [staging_branch_option, {
|
|
13
|
+
name: :workflows,
|
|
14
|
+
params: {
|
|
15
|
+
type: :array, banner: "FILE...",
|
|
16
|
+
desc: "Explicitly add or replace these generated workflow filenames (default: preserve all workflows)"
|
|
17
|
+
}
|
|
18
|
+
}].freeze
|
|
19
|
+
DESCRIPTION = "Regenerates GitHub Actions files for the installed cpflow version"
|
|
14
20
|
LONG_DESCRIPTION = <<~DESC.freeze
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Refreshes local composite actions and helper files from the installed gem.
|
|
22
|
+
All top-level workflows are preserved by default, including their refs,
|
|
23
|
+
triggers, permissions, and deployment ownership. Use --workflows FILE...
|
|
24
|
+
to explicitly add or replace named generated workflows. Replacement resets
|
|
25
|
+
each selected workflow to its template and matching v#{Cpflow::VERSION} ref;
|
|
26
|
+
review and reapply any downstream customizations in the same PR.
|
|
27
|
+
|
|
28
|
+
Selecting cpflow-deploy-staging.yml preserves a single existing push branch
|
|
29
|
+
or the default main/master pair. Missing or ambiguous branches require
|
|
30
|
+
--staging-branch BRANCH. This option requires selecting the staging workflow.
|
|
31
|
+
|
|
32
|
+
A differing existing bin/test-cpflow-github-flow blocks all writes. Move
|
|
33
|
+
downstream checks to executable bin/test-cpflow-github-flow-custom and
|
|
34
|
+
remove or rename the old validator before updating. The generated validator
|
|
35
|
+
invokes that extension after its baseline checks; the extension is never
|
|
36
|
+
generated or overwritten. See docs/ci-automation.md for migration guidance.
|
|
23
37
|
DESC
|
|
24
38
|
EXAMPLES = <<~EX
|
|
25
39
|
```sh
|
|
26
|
-
#
|
|
40
|
+
# Refresh actions/helpers while preserving downstream workflows
|
|
27
41
|
cpflow update-github-actions
|
|
28
42
|
|
|
29
43
|
# When running cpflow through Bundler
|
|
30
44
|
bundle exec cpflow update-github-actions
|
|
31
45
|
|
|
32
|
-
#
|
|
33
|
-
cpflow update-github-actions --
|
|
46
|
+
# Explicitly replace the review-app wrapper
|
|
47
|
+
cpflow update-github-actions --workflows cpflow-deploy-review-app.yml
|
|
48
|
+
|
|
49
|
+
# Explicitly add or replace staging deployment from develop
|
|
50
|
+
cpflow update-github-actions --workflows cpflow-deploy-staging.yml --staging-branch develop
|
|
34
51
|
```
|
|
35
52
|
EX
|
|
36
53
|
WITH_INFO_HEADER = false
|
|
@@ -43,66 +60,88 @@ module Command
|
|
|
43
60
|
def call
|
|
44
61
|
GenerateGithubActions.ensure_template_root!
|
|
45
62
|
abort_if_no_generated_files!
|
|
46
|
-
|
|
47
|
-
branch =
|
|
48
|
-
|
|
63
|
+
workflows = selected_workflows
|
|
64
|
+
branch = selected_staging_branch(workflows)
|
|
65
|
+
abort_if_custom_validator!
|
|
66
|
+
GithubActionsGenerator.new([branch].compact, workflows: workflows).invoke_all
|
|
49
67
|
|
|
50
68
|
print_post_update_message
|
|
51
69
|
end
|
|
52
70
|
|
|
53
71
|
private
|
|
54
72
|
|
|
73
|
+
def selected_workflows
|
|
74
|
+
available = GenerateGithubActions.generated_files.grep(%r{\A\.github/workflows/}).map do |path|
|
|
75
|
+
File.basename(path)
|
|
76
|
+
end
|
|
77
|
+
selected = Array(config.options[:workflows])
|
|
78
|
+
unknown = selected - available
|
|
79
|
+
Shell.abort("Unknown --workflows: #{unknown.join(', ')}. Available: #{available.join(', ')}") if unknown.any?
|
|
80
|
+
selected
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def selected_staging_branch(workflows)
|
|
84
|
+
branch = staging_branch
|
|
85
|
+
return branch || inferred_staging_branch if workflows.include?(STAGING_WORKFLOW_PATH.basename.to_s)
|
|
86
|
+
|
|
87
|
+
Shell.abort("--staging-branch requires --workflows cpflow-deploy-staging.yml") if branch
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def abort_if_custom_validator!
|
|
92
|
+
path = "bin/test-cpflow-github-flow"
|
|
93
|
+
return unless File.exist?(path)
|
|
94
|
+
return if File.binread(path) == File.binread(GenerateGithubActions.source_file(path))
|
|
95
|
+
|
|
96
|
+
Shell.abort(<<~MESSAGE)
|
|
97
|
+
#{path} differs from the current generated validator. No files were updated.
|
|
98
|
+
Move downstream checks to executable bin/test-cpflow-github-flow-custom, then remove or rename
|
|
99
|
+
#{path} and retry. See docs/ci-automation.md for the legacy validator migration.
|
|
100
|
+
MESSAGE
|
|
101
|
+
end
|
|
102
|
+
|
|
55
103
|
def abort_if_no_generated_files!
|
|
56
104
|
return if GenerateGithubActions.generated_files.any? { |path| File.exist?(path) }
|
|
57
105
|
|
|
58
|
-
Shell.abort(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
Shell.abort(<<~MESSAGE)
|
|
107
|
+
No generated cpflow GitHub Actions files found in this repository.
|
|
108
|
+
Run `cpflow generate-github-actions` first to create the wrappers,
|
|
109
|
+
then use `cpflow update-github-actions` after future gem upgrades.
|
|
110
|
+
MESSAGE
|
|
63
111
|
end
|
|
64
112
|
|
|
65
113
|
def print_post_update_message
|
|
66
114
|
Shell.info("")
|
|
67
|
-
Shell.info("Updated cpflow GitHub Actions
|
|
115
|
+
Shell.info("Updated cpflow GitHub Actions files for cpflow #{Cpflow::VERSION}.")
|
|
116
|
+
Shell.info("Preserved unselected workflows; review their refs and compatibility before committing.")
|
|
68
117
|
Shell.info("Next: review the diff and run `bin/test-cpflow-github-flow`.")
|
|
69
118
|
Shell.info("If you run cpflow through Bundler, use `bin/test-cpflow-github-flow bundle exec cpflow`.")
|
|
70
119
|
end
|
|
71
120
|
|
|
72
121
|
def inferred_staging_branch
|
|
73
122
|
branches = existing_staging_branches
|
|
74
|
-
return if branches.
|
|
75
|
-
return branches.first if branches.length == 1
|
|
76
|
-
|
|
77
|
-
Shell.warn(
|
|
78
|
-
"Existing staging workflow has multiple custom push branches: #{branches.join(', ')}. " \
|
|
79
|
-
"Run with --staging-branch BRANCH if only one branch should auto-deploy staging."
|
|
80
|
-
)
|
|
123
|
+
return if branches.sort == DEFAULT_STAGING_BRANCHES.sort
|
|
124
|
+
return branches.first if branches.length == 1 && valid_staging_branch?(branches.first)
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
Shell.abort("Cannot infer staging deployment branch. Pass --staging-branch BRANCH to explicitly set it.")
|
|
83
127
|
end
|
|
84
128
|
|
|
85
129
|
def existing_staging_branches
|
|
86
|
-
|
|
130
|
+
push = parsed_staging_workflow_on["push"]
|
|
131
|
+
branches = push.is_a?(Hash) ? push["branches"] : nil
|
|
132
|
+
branches.is_a?(Array) && branches.all?(String) ? branches : []
|
|
87
133
|
end
|
|
88
134
|
|
|
89
135
|
def parsed_staging_workflow_on
|
|
90
136
|
return {} unless STAGING_WORKFLOW_PATH.file?
|
|
91
137
|
|
|
92
138
|
workflow = YAML.load_file(STAGING_WORKFLOW_PATH, aliases: true)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
workflow_on = workflow["on"] || workflow[true]
|
|
139
|
+
workflow_on = workflow.is_a?(Hash) ? workflow["on"] || workflow[true] : nil
|
|
96
140
|
workflow_on.is_a?(Hash) ? workflow_on : {}
|
|
97
141
|
rescue Psych::SyntaxError => e
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def warn_unparseable_staging_workflow(error)
|
|
103
|
-
Shell.warn(
|
|
104
|
-
"Could not parse #{STAGING_WORKFLOW_PATH}: #{error.message}. " \
|
|
105
|
-
"Run with --staging-branch BRANCH if staging should use a custom branch."
|
|
142
|
+
Shell.abort(
|
|
143
|
+
"Could not parse #{STAGING_WORKFLOW_PATH}: #{e.message}. " \
|
|
144
|
+
"Pass --staging-branch BRANCH to explicitly replace its staging deployment branch."
|
|
106
145
|
)
|
|
107
146
|
end
|
|
108
147
|
end
|
data/lib/core/config.rb
CHANGED
|
@@ -10,6 +10,7 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
10
10
|
include Helpers
|
|
11
11
|
|
|
12
12
|
CONFIG_FILE_LOCATION = ".controlplane/controlplane.yml"
|
|
13
|
+
GENERATED_REVIEW_APP_TAG = "cpflow-generated-review-app"
|
|
13
14
|
REQUIRED_SHARED_SECRET_GRANT_KEYS = %i[name secret_name policy_name].freeze
|
|
14
15
|
SHARED_SECRET_RESOURCE_NAME_KEYS = %i[secret_name policy_name].freeze
|
|
15
16
|
CONTROL_PLANE_RESOURCE_NAME_REGEX = /\A[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z/
|
|
@@ -52,6 +53,8 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def secrets
|
|
56
|
+
return "#{app}-secrets" if generated_review_secret_keys.any?
|
|
57
|
+
|
|
55
58
|
current&.dig(:secrets_name) || "#{app_prefix}-secrets"
|
|
56
59
|
end
|
|
57
60
|
|
|
@@ -59,6 +62,54 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
59
62
|
current&.dig(:secrets_policy_name) || "#{secrets}-policy"
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
def generated_review_secret_keys
|
|
66
|
+
@generated_review_secret_keys ||= begin
|
|
67
|
+
keys = current&.dig(:generated_review_secret_keys)
|
|
68
|
+
if keys.nil?
|
|
69
|
+
[]
|
|
70
|
+
else
|
|
71
|
+
validate_generated_review_secret_scope!
|
|
72
|
+
validate_generated_review_secret_keys!(keys)
|
|
73
|
+
keys
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Cleanup must still find marked per-app resources after the opt-in is removed
|
|
79
|
+
# from the current configuration.
|
|
80
|
+
def disposable_review_secret_resource_names
|
|
81
|
+
return nil unless current&.dig(:match_if_app_name_starts_with) && app.to_s != app_prefix.to_s
|
|
82
|
+
return nil unless app.match?(CONTROL_PLANE_RESOURCE_NAME_REGEX)
|
|
83
|
+
|
|
84
|
+
secret = "#{app}-secrets"
|
|
85
|
+
policy = "#{secret}-policy"
|
|
86
|
+
return nil unless policy.length <= 64
|
|
87
|
+
|
|
88
|
+
[secret, policy]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_generated_review_secret_scope!
|
|
92
|
+
dynamic_review_app = current[:match_if_app_name_starts_with] && app.to_s != app_prefix.to_s
|
|
93
|
+
raise "generated_review_secret_keys is only allowed for a dynamically named review app." unless dynamic_review_app
|
|
94
|
+
|
|
95
|
+
unless "#{app}-secrets-policy".length <= 64 && app.match?(CONTROL_PLANE_RESOURCE_NAME_REGEX)
|
|
96
|
+
raise "Review app name is too long or invalid for a per-app secret policy."
|
|
97
|
+
end
|
|
98
|
+
return unless current[:secrets_name] || current[:secrets_policy_name]
|
|
99
|
+
|
|
100
|
+
raise "generated_review_secret_keys cannot be combined with secrets_name or secrets_policy_name."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_generated_review_secret_keys!(keys)
|
|
104
|
+
valid_size = keys.is_a?(Array) && keys.size.between?(1, 8)
|
|
105
|
+
valid_names = valid_size && keys.all? { |key| key.is_a?(String) && key.match?(/\A[A-Z][A-Z0-9_]*\z/) }
|
|
106
|
+
return if valid_names && keys.uniq == keys
|
|
107
|
+
|
|
108
|
+
raise "generated_review_secret_keys must contain 1-8 unique uppercase environment names."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private :validate_generated_review_secret_scope!, :validate_generated_review_secret_keys!
|
|
112
|
+
|
|
62
113
|
def shared_secret_grants
|
|
63
114
|
@shared_secret_grants ||= normalize_shared_secret_grants(current&.dig(:shared_secret_grants))
|
|
64
115
|
end
|
|
@@ -188,6 +239,28 @@ class Config # rubocop:disable Metrics/ClassLength
|
|
|
188
239
|
end
|
|
189
240
|
end
|
|
190
241
|
|
|
242
|
+
def validate_generated_review_secret_settings!
|
|
243
|
+
apps.each do |app_name, app_options|
|
|
244
|
+
keys = app_options[:generated_review_secret_keys]
|
|
245
|
+
next if keys.nil?
|
|
246
|
+
|
|
247
|
+
validate_generated_review_secret_entry_scope!(app_name, app_options)
|
|
248
|
+
validate_generated_review_secret_keys!(keys)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def validate_generated_review_secret_entry_scope!(app_name, app_options)
|
|
253
|
+
unless app_options[:match_if_app_name_starts_with]
|
|
254
|
+
raise "generated_review_secret_keys for '#{app_name}' requires a dynamically matched review app."
|
|
255
|
+
end
|
|
256
|
+
return unless app_options[:secrets_name] || app_options[:secrets_policy_name]
|
|
257
|
+
|
|
258
|
+
raise "generated_review_secret_keys for '#{app_name}' cannot be combined with " \
|
|
259
|
+
"secrets_name or secrets_policy_name."
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
private :validate_generated_review_secret_entry_scope!
|
|
263
|
+
|
|
191
264
|
private
|
|
192
265
|
|
|
193
266
|
def ensure_current_config!
|