cpflow 5.0.0.rc.1 → 5.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/{lib/github_flow_templates/.github → .github}/actions/cpflow-delete-control-plane-app/action.yml +5 -0
- data/{lib/github_flow_templates/.github → .github}/actions/cpflow-detect-release-phase/action.yml +7 -0
- data/.github/actions/cpflow-setup-environment/action.yml +161 -0
- data/.github/workflows/cpflow-cleanup-stale-review-apps.yml +69 -0
- data/.github/workflows/cpflow-delete-review-app.yml +182 -0
- data/.github/workflows/cpflow-deploy-review-app.yml +507 -0
- data/.github/workflows/cpflow-deploy-staging.yml +168 -0
- data/.github/workflows/cpflow-help-command.yml +78 -0
- data/.github/workflows/cpflow-promote-staging-to-production.yml +510 -0
- data/.github/workflows/cpflow-review-app-help.yml +51 -0
- data/.github/workflows/rspec-shared.yml +3 -0
- data/.github/workflows/trigger-docs-site.yml +90 -0
- data/.rubocop.yml +14 -1
- data/CHANGELOG.md +43 -1
- data/CONTRIBUTING.md +27 -0
- data/Gemfile.lock +2 -2
- data/README.md +7 -3
- data/cpflow.gemspec +1 -1
- data/docs/ai-github-flow-prompt.md +1 -1
- data/docs/assets/cpflow-deploying.svg +46 -0
- data/docs/ci-automation.md +111 -8
- data/docs/commands.md +11 -5
- data/docs/thruster.md +149 -0
- data/docs/troubleshooting.md +8 -0
- data/lib/command/apply_template.rb +6 -2
- data/lib/command/base.rb +1 -0
- data/lib/command/cleanup_stale_apps.rb +53 -14
- data/lib/command/delete.rb +3 -1
- data/lib/command/deploy_image.rb +5 -2
- data/lib/command/generate.rb +7 -3
- data/lib/command/generate_github_actions.rb +21 -9
- data/lib/command/generator_helpers.rb +5 -1
- data/lib/command/info.rb +3 -1
- data/lib/command/run.rb +16 -1
- data/lib/command/test.rb +1 -3
- data/lib/core/controlplane.rb +17 -6
- data/lib/core/controlplane_api.rb +3 -1
- data/lib/core/controlplane_api_direct.rb +50 -27
- data/lib/core/doctor_service.rb +2 -2
- data/lib/core/github_flow_readiness_service.rb +26 -2
- data/lib/core/repo_introspection.rb +41 -3
- data/lib/core/shell.rb +3 -1
- data/lib/core/terraform_config/policy.rb +1 -1
- data/lib/cpflow/version.rb +1 -1
- data/lib/cpflow.rb +27 -13
- data/lib/generator_templates/templates/rails.yml +4 -0
- data/lib/generator_templates_sqlite/templates/rails.yml +4 -0
- data/lib/github_flow_templates/.github/cpflow-help.md +30 -1
- data/lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml +10 -44
- data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +15 -114
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +10 -413
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +12 -123
- data/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +10 -33
- data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +13 -475
- data/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +12 -30
- data/lib/github_flow_templates/bin/pin-cpflow-github-ref +72 -0
- data/lib/github_flow_templates/bin/test-cpflow-github-flow +89 -0
- data/rakelib/create_release.rake +4 -4
- metadata +26 -17
- data/lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml +0 -98
- /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-build-docker-image/action.yml +0 -0
- /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-delete-control-plane-app/delete-app.sh +0 -0
- /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-validate-config/action.yml +0 -0
- /data/{lib/github_flow_templates/.github → .github}/actions/cpflow-wait-for-health/action.yml +0 -0
data/docs/thruster.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Thruster HTTP/2 Proxy on Control Plane
|
|
2
|
+
|
|
3
|
+
[Thruster](https://github.com/basecamp/thruster) is Basecamp's zero-config HTTP/2 proxy for
|
|
4
|
+
Ruby web applications. It provides HTTP/2 support, asset caching, compression, and early
|
|
5
|
+
hints. Running it on Control Plane requires settings that differ from a standalone (e.g.,
|
|
6
|
+
VPS) deployment, and getting them wrong produces a confusing `502 Bad Gateway` with a
|
|
7
|
+
"protocol error" message.
|
|
8
|
+
|
|
9
|
+
This page documents the configuration that works.
|
|
10
|
+
|
|
11
|
+
## TL;DR
|
|
12
|
+
|
|
13
|
+
- Workload port: `protocol: http` (not `http2`).
|
|
14
|
+
- Dockerfile `CMD` runs Thruster: `CMD ["bundle", "exec", "thrust", "bin/rails", "server"]`.
|
|
15
|
+
- End users still get HTTP/2; Control Plane's load balancer handles TLS termination.
|
|
16
|
+
|
|
17
|
+
## Why `protocol: http` and not `http2`
|
|
18
|
+
|
|
19
|
+
### Standalone Thruster (e.g., VPS)
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
User → HTTPS/HTTP2 → Thruster → HTTP/1.1 → Rails
|
|
23
|
+
(Thruster handles TLS + HTTP/2)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Control Plane + Thruster
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
User → HTTPS/HTTP2 → Control Plane LB → HTTP/1.1 → Thruster → HTTP/1.1 → Rails
|
|
30
|
+
(LB handles TLS) (protocol: http) (caching, compression)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
In the diagram above, `protocol: http` is the workload port setting that governs the
|
|
34
|
+
LB→Thruster hop; `caching, compression` describes what Thruster contributes in this
|
|
35
|
+
path.
|
|
36
|
+
|
|
37
|
+
On Control Plane, the load balancer terminates TLS and speaks HTTP/2 to the browser —
|
|
38
|
+
so Thruster never sees TLS or HTTP/2 on its incoming side. This is the opposite of a
|
|
39
|
+
standalone (VPS) deployment, where Thruster itself handles TLS and HTTP/2. Setting
|
|
40
|
+
`protocol: http2` on the workload port tells the load balancer to expect HTTP/2 from
|
|
41
|
+
the container, which Thruster does not emit on that hop, and protocol negotiation fails
|
|
42
|
+
with `502 Bad Gateway`.
|
|
43
|
+
|
|
44
|
+
Even with `protocol: http`, end users still get:
|
|
45
|
+
|
|
46
|
+
- HTTP/2 to the browser (from the Control Plane load balancer)
|
|
47
|
+
- HTTP/2 multiplexing (from the Control Plane load balancer)
|
|
48
|
+
- Asset caching and compression (from Thruster)
|
|
49
|
+
- Efficient static file serving (from Thruster)
|
|
50
|
+
- Early Hints (103) from Thruster (reaches the browser only if the load balancer forwards 103 responses)
|
|
51
|
+
|
|
52
|
+
## Workload template
|
|
53
|
+
|
|
54
|
+
In `.controlplane/templates/rails.yml`:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
ports:
|
|
58
|
+
- number: 3000
|
|
59
|
+
protocol: http # Required when fronting Rails with Thruster. Do not use http2.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Dockerfile
|
|
63
|
+
|
|
64
|
+
The container's `CMD` must launch Thruster. On Control Plane/Kubernetes the Dockerfile
|
|
65
|
+
`CMD` determines container startup — the `Procfile` is not used (unlike Heroku).
|
|
66
|
+
|
|
67
|
+
```dockerfile
|
|
68
|
+
# .controlplane/Dockerfile
|
|
69
|
+
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Troubleshooting
|
|
73
|
+
|
|
74
|
+
### `502 Bad Gateway` with "protocol error"
|
|
75
|
+
|
|
76
|
+
The workload port is set to `protocol: http2`. Change it to `protocol: http` in
|
|
77
|
+
`rails.yml`, then push the workload spec.
|
|
78
|
+
|
|
79
|
+
`cpflow apply-template` rewrites the workload from the template. If you have tuned
|
|
80
|
+
CPU, memory, autoscaling, firewall, or other workload fields directly in the
|
|
81
|
+
Control Plane UI (or via `cpln`) without mirroring those changes back into
|
|
82
|
+
`rails.yml`, those edits will be reset. Either reconcile `rails.yml` with the live
|
|
83
|
+
spec first, or change the port field in place:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
# Option A — apply the full template (resets any drift between rails.yml and the live spec):
|
|
87
|
+
cpflow apply-template rails -a <app>
|
|
88
|
+
|
|
89
|
+
# Option B — edit only the port protocol in place (preserves UI-tuned fields):
|
|
90
|
+
cpln workload edit <workload> --gvc <gvc> --org <org>
|
|
91
|
+
# change spec.containers[].ports[].protocol from http2 to http
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Inspect the current spec before choosing if you're unsure what would change:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
cpln workload get <workload> --gvc <gvc> --org <org> -o yaml
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Note: `cpflow deploy-image` alone is not sufficient — it only updates the container
|
|
101
|
+
image reference and does not modify the workload's port configuration. Run it
|
|
102
|
+
*after* the protocol change has been applied if you also want to ship a new image.
|
|
103
|
+
|
|
104
|
+
The remaining troubleshooting commands use the raw Control Plane CLI (`cpln`) rather
|
|
105
|
+
than `cpflow`; see
|
|
106
|
+
[the Control Plane CLI quickstart](https://shakadocs.controlplane.com/quickstart/quick-start-3-cli#getting-started-with-the-cli)
|
|
107
|
+
if you don't already have it installed.
|
|
108
|
+
|
|
109
|
+
### Verify Thruster is the process running as PID 1
|
|
110
|
+
|
|
111
|
+
`/proc/1/cmdline` stores arguments NUL-separated with no trailing newline, so pipe it
|
|
112
|
+
through `tr` to make the output readable:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
cpln workload exec <workload> --gvc <gvc> --org <org> --location <location> \
|
|
116
|
+
-- sh -c "tr '\0' ' ' < /proc/1/cmdline && echo"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You should see `thrust` in the output. If you see `rails` or `puma` directly, the
|
|
120
|
+
Dockerfile `CMD` is not invoking Thruster.
|
|
121
|
+
|
|
122
|
+
### Test HTTP connectivity through Thruster
|
|
123
|
+
|
|
124
|
+
This hits Thruster on port 3000 — not Rails directly. A `200 OK` confirms the
|
|
125
|
+
Thruster → Rails path within the container is healthy.
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
cpln workload exec <workload> --gvc <gvc> --org <org> --location <location> \
|
|
129
|
+
-- curl -s localhost:3000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Inspect the workload port configuration
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
cpln workload get <workload> --gvc <gvc> --org <org> -o json | jq '.spec.containers[].ports'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Reference implementation
|
|
139
|
+
|
|
140
|
+
A working setup lives in the
|
|
141
|
+
[react-webpack-rails-tutorial](https://github.com/shakacode/react-webpack-rails-tutorial)
|
|
142
|
+
repository — see
|
|
143
|
+
[`.controlplane/templates/rails.yml`](https://github.com/shakacode/react-webpack-rails-tutorial/blob/master/.controlplane/templates/rails.yml)
|
|
144
|
+
on the `master` branch.
|
|
145
|
+
|
|
146
|
+
## Further reading
|
|
147
|
+
|
|
148
|
+
- [Thruster on GitHub](https://github.com/basecamp/thruster)
|
|
149
|
+
- [DHH on Rails 8 with Thruster](https://world.hey.com/dhh/rails-8-with-thruster-by-default-c953f5e3)
|
data/docs/troubleshooting.md
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
4
4
|
## App Web Page Shows `upstream request timeout`
|
|
5
5
|
|
|
6
6
|
If you get a blank screen showing the message `upstream request timeout` on your app after running `cpflow open -a my-app-name`, check out the application logs. Your image has been promoted and your app crashing when starting.
|
|
7
|
+
|
|
8
|
+
## `502 Bad Gateway` with "protocol error" (Rails + Thruster)
|
|
9
|
+
|
|
10
|
+
If a Rails app fronted by [Thruster](https://github.com/basecamp/thruster) returns `502 Bad Gateway` with a "protocol error" message, the workload port is likely set to `protocol: http2`.
|
|
11
|
+
|
|
12
|
+
Thruster accepts HTTP/1.1 connections from the load balancer, so the workload port must be `protocol: http`. Control Plane's load balancer still serves HTTP/2 to end users.
|
|
13
|
+
|
|
14
|
+
See [Thruster HTTP/2 Proxy on Control Plane](./thruster.md) for the full configuration and debugging commands.
|
|
@@ -91,6 +91,10 @@ module Command
|
|
|
91
91
|
raise "Can't find templates above, please create them."
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
# The confirm_* helpers below prompt the user and mutate state (@asked_for_confirmation,
|
|
95
|
+
# report_skipped). `confirm_app` and `confirm_workload` return booleans but have side
|
|
96
|
+
# effects, so their names intentionally lack `?` (suppressed via the inline disables
|
|
97
|
+
# below). `confirm_apply` doesn't trip the cop and needs no annotation.
|
|
94
98
|
def confirm_apply(message)
|
|
95
99
|
return true if config.options[:yes]
|
|
96
100
|
|
|
@@ -98,7 +102,7 @@ module Command
|
|
|
98
102
|
Shell.confirm(message)
|
|
99
103
|
end
|
|
100
104
|
|
|
101
|
-
def confirm_app(template)
|
|
105
|
+
def confirm_app(template) # rubocop:disable Naming/PredicateMethod
|
|
102
106
|
app = cp.fetch_gvc(template["name"])
|
|
103
107
|
return true unless app
|
|
104
108
|
|
|
@@ -109,7 +113,7 @@ module Command
|
|
|
109
113
|
false
|
|
110
114
|
end
|
|
111
115
|
|
|
112
|
-
def confirm_workload(template)
|
|
116
|
+
def confirm_workload(template) # rubocop:disable Naming/PredicateMethod
|
|
113
117
|
workload = cp.fetch_workload(template["name"])
|
|
114
118
|
return true unless workload
|
|
115
119
|
|
data/lib/command/base.rb
CHANGED
|
@@ -2,20 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
module Command
|
|
4
4
|
class CleanupStaleApps < Base
|
|
5
|
+
CLEANUP_MODE_OPTION = {
|
|
6
|
+
name: :mode,
|
|
7
|
+
params: {
|
|
8
|
+
banner: "MODE",
|
|
9
|
+
desc: "Action to take on stale apps: `delete` (default) or `stop`",
|
|
10
|
+
type: :string,
|
|
11
|
+
required: false,
|
|
12
|
+
default: "delete",
|
|
13
|
+
valid_regex: /^(delete|stop)$/
|
|
14
|
+
}
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
5
17
|
NAME = "cleanup-stale-apps"
|
|
6
18
|
OPTIONS = [
|
|
7
19
|
app_option(required: true),
|
|
8
|
-
skip_confirm_option
|
|
20
|
+
skip_confirm_option,
|
|
21
|
+
CLEANUP_MODE_OPTION
|
|
9
22
|
].freeze
|
|
10
|
-
DESCRIPTION = "Deletes
|
|
23
|
+
DESCRIPTION = "Deletes or stops stale apps based on the latest image's creation date"
|
|
11
24
|
LONG_DESCRIPTION = <<~DESC
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
25
|
+
- Acts on stale apps based on the creation date of the latest image, or the GVC if no images exist
|
|
26
|
+
- With `--mode=delete` (default): deletes the whole app (GVC with all workloads, all volumesets and all images), and unbinds the app from the secrets policy as long as both the identity and the policy exist (and are bound)
|
|
27
|
+
- With `--mode=stop`: suspends all workloads via `cpflow ps:stop` — no GVC, volumeset, or image is removed; resume with `cpflow ps:start`
|
|
28
|
+
- `--mode=stop` only suspends workloads listed in `app_workloads` + `additional_workloads`; workloads present in the live GVC but missing from the config are skipped silently
|
|
29
|
+
- `--mode=stop` returns once each workload is marked suspended; it does not wait for the workload to reach a not-ready state
|
|
15
30
|
- Specify the amount of days after an app should be considered stale through `stale_app_image_deployed_days` in the `.controlplane/controlplane.yml` file
|
|
16
|
-
- If `match_if_app_name_starts_with` is `true` in the `.controlplane/controlplane.yml` file, it will
|
|
31
|
+
- If `match_if_app_name_starts_with` is `true` in the `.controlplane/controlplane.yml` file, it will act on all stale apps that start with the name
|
|
17
32
|
- Will ask for explicit user confirmation
|
|
18
33
|
DESC
|
|
34
|
+
EXAMPLES = <<~EX
|
|
35
|
+
```sh
|
|
36
|
+
# Deletes stale apps (default).
|
|
37
|
+
cpflow cleanup-stale-apps -a $APP_NAME
|
|
38
|
+
|
|
39
|
+
# Stops stale apps instead of deleting them; resume with `cpflow ps:start`.
|
|
40
|
+
cpflow cleanup-stale-apps -a $APP_NAME --mode=stop
|
|
41
|
+
```
|
|
42
|
+
EX
|
|
19
43
|
|
|
20
44
|
def call # rubocop:disable Metrics/MethodLength
|
|
21
45
|
return progress.puts("No stale apps found.") if stale_apps.empty?
|
|
@@ -25,11 +49,11 @@ module Command
|
|
|
25
49
|
progress.puts(" - #{app[:name]} (#{Shell.color(app[:date].to_s, :red)})")
|
|
26
50
|
end
|
|
27
51
|
|
|
28
|
-
return unless
|
|
52
|
+
return unless confirm_action
|
|
29
53
|
|
|
30
54
|
progress.puts
|
|
31
55
|
stale_apps.each do |app|
|
|
32
|
-
|
|
56
|
+
process_app(app[:name])
|
|
33
57
|
progress.puts
|
|
34
58
|
end
|
|
35
59
|
end
|
|
@@ -50,9 +74,11 @@ module Command
|
|
|
50
74
|
|
|
51
75
|
images = cp.query_images(app_name)["items"].select { |item| item["name"].start_with?("#{app_name}:") }
|
|
52
76
|
image = cp.latest_image_from(images, app_name: app_name, name_only: false)
|
|
53
|
-
next unless image
|
|
54
77
|
|
|
55
|
-
|
|
78
|
+
created_at = image ? image["created"] : gvc["created"]
|
|
79
|
+
next unless created_at
|
|
80
|
+
|
|
81
|
+
created_date = DateTime.parse(created_at)
|
|
56
82
|
diff_in_days = (now - created_date).to_i
|
|
57
83
|
next unless diff_in_days >= stale_app_image_deployed_days
|
|
58
84
|
|
|
@@ -66,14 +92,27 @@ module Command
|
|
|
66
92
|
end
|
|
67
93
|
end
|
|
68
94
|
|
|
69
|
-
def
|
|
95
|
+
def confirm_action
|
|
70
96
|
return true if config.options[:yes]
|
|
71
97
|
|
|
72
|
-
Shell.confirm("\nAre you sure you want to
|
|
98
|
+
Shell.confirm("\nAre you sure you want to #{action_description} these #{stale_apps.length} apps?")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def process_app(app)
|
|
102
|
+
if mode == "stop"
|
|
103
|
+
progress.puts("Stopping app '#{app}'")
|
|
104
|
+
run_cpflow_command("ps:stop", "-a", app)
|
|
105
|
+
else
|
|
106
|
+
run_cpflow_command("delete", "-a", app, "--yes")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def action_description
|
|
111
|
+
mode == "stop" ? "suspend all workloads in" : "delete"
|
|
73
112
|
end
|
|
74
113
|
|
|
75
|
-
def
|
|
76
|
-
|
|
114
|
+
def mode
|
|
115
|
+
@mode ||= config.options[:mode]
|
|
77
116
|
end
|
|
78
117
|
end
|
|
79
118
|
end
|
data/lib/command/delete.rb
CHANGED
|
@@ -81,7 +81,9 @@ module Command
|
|
|
81
81
|
progress.puts("#{Shell.color(message, :red)}\n#{images_list}\n\n")
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
# Prompts the user and writes to progress on confirm — returns boolean but
|
|
85
|
+
# has side effects, so the method name intentionally lacks `?`.
|
|
86
|
+
def confirm_delete(item) # rubocop:disable Naming/PredicateMethod
|
|
85
87
|
return true if config.options[:yes]
|
|
86
88
|
|
|
87
89
|
confirmed = Shell.confirm("Are you sure you want to delete '#{item}'?")
|
data/lib/command/deploy_image.rb
CHANGED
|
@@ -31,10 +31,13 @@ module Command
|
|
|
31
31
|
next unless container["image"].match?(%r{^/org/#{config.org}/image/#{config.app}[:@]})
|
|
32
32
|
|
|
33
33
|
container_name = container["name"]
|
|
34
|
-
step("Deploying image '#{image}' for workload '#{
|
|
34
|
+
step("Deploying image '#{image}' for workload '#{workload}'") do
|
|
35
35
|
cp.workload_set_image_ref(workload, container: container_name, image: image)
|
|
36
|
-
deployed_endpoints[
|
|
36
|
+
deployed_endpoints[workload] = endpoint_for_workload(workload_data)
|
|
37
37
|
end
|
|
38
|
+
# Deploy the first matching app-image container per workload; CPLN workloads
|
|
39
|
+
# are expected to have a single container that runs the app image.
|
|
40
|
+
break
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
|
data/lib/command/generate.rb
CHANGED
|
@@ -93,9 +93,13 @@ module Command
|
|
|
93
93
|
def asset_precompile_hook_run
|
|
94
94
|
command = normalized_asset_precompile_hook_command
|
|
95
95
|
return "" unless command
|
|
96
|
-
return "" unless single_line_asset_precompile_hook?(command)
|
|
97
96
|
|
|
98
|
-
|
|
97
|
+
# Folded YAML scalars carry a trailing newline even when they hold one command.
|
|
98
|
+
stripped = command.strip
|
|
99
|
+
return "" if stripped.empty?
|
|
100
|
+
return "" unless single_line_asset_precompile_hook?(stripped)
|
|
101
|
+
|
|
102
|
+
"RUN #{stripped}\n\n"
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
def single_line_asset_precompile_hook?(command)
|
|
@@ -122,7 +126,7 @@ module Command
|
|
|
122
126
|
# Parse rather than regex-match: Shakapacker emits an environment-keyed YAML file
|
|
123
127
|
# (the hook usually lives under `default:` or `production:`), and folded or quoted
|
|
124
128
|
# multi-line values would also defeat a single-line regex.
|
|
125
|
-
config = YAML.
|
|
129
|
+
config = YAML.safe_load_file("config/shakapacker.yml", aliases: true)
|
|
126
130
|
hook = extract_shakapacker_precompile_hook(config)
|
|
127
131
|
hook unless hook.nil? || hook.empty?
|
|
128
132
|
rescue Psych::SyntaxError
|
|
@@ -14,8 +14,9 @@ module Command
|
|
|
14
14
|
|
|
15
15
|
def copy_files
|
|
16
16
|
relative_paths = generated_files
|
|
17
|
+
replacements = template_variables
|
|
17
18
|
copy_template_files(relative_paths)
|
|
18
|
-
substitute_template_variables(relative_paths)
|
|
19
|
+
substitute_template_variables(relative_paths, replacements)
|
|
19
20
|
make_shell_scripts_executable(relative_paths)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -39,9 +40,9 @@ module Command
|
|
|
39
40
|
|
|
40
41
|
def template_variables
|
|
41
42
|
{
|
|
42
|
-
"
|
|
43
|
+
"__CPFLOW_GITHUB_ACTIONS_REF__" => cpflow_github_actions_ref,
|
|
43
44
|
"__STAGING_BRANCH_FILTER__" => staging_branch_filter,
|
|
44
|
-
"
|
|
45
|
+
"__STAGING_BRANCH_DEFAULT__" => staging_branch_default
|
|
45
46
|
}
|
|
46
47
|
end
|
|
47
48
|
|
|
@@ -58,12 +59,23 @@ module Command
|
|
|
58
59
|
branches.map(&:to_json).join(", ")
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def
|
|
62
|
-
|
|
62
|
+
def staging_branch_default
|
|
63
|
+
staging_branch.to_s
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cpflow_github_actions_ref
|
|
67
|
+
ref = ENV.fetch("CPFLOW_GITHUB_ACTIONS_REF", default_cpflow_github_actions_ref).to_s.strip
|
|
68
|
+
return default_cpflow_github_actions_ref if ref.empty?
|
|
69
|
+
|
|
70
|
+
if ref.match?(/[[:space:]]/)
|
|
71
|
+
Shell.abort("Invalid CPFLOW_GITHUB_ACTIONS_REF: #{ref.inspect}. Refs cannot contain whitespace.")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
ref
|
|
75
|
+
end
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
"${{ vars.STAGING_APP_BRANCH || '#{staging_branch}' }}"
|
|
77
|
+
def default_cpflow_github_actions_ref
|
|
78
|
+
"v#{::Cpflow::VERSION}"
|
|
67
79
|
end
|
|
68
80
|
end
|
|
69
81
|
|
|
@@ -84,7 +96,7 @@ module Command
|
|
|
84
96
|
DESC
|
|
85
97
|
EXAMPLES = <<~EX
|
|
86
98
|
```sh
|
|
87
|
-
# Creates
|
|
99
|
+
# Creates thin .github/workflows wrappers for the Control Plane flow
|
|
88
100
|
cpflow generate-github-actions
|
|
89
101
|
|
|
90
102
|
# Creates the flow with staging deploys triggered from develop
|
|
@@ -22,10 +22,14 @@ module Command
|
|
|
22
22
|
|
|
23
23
|
def make_shell_scripts_executable(file_paths)
|
|
24
24
|
Array(file_paths).each do |path|
|
|
25
|
-
next unless File.file?(path) &&
|
|
25
|
+
next unless File.file?(path) && executable_script?(path)
|
|
26
26
|
|
|
27
27
|
FileUtils.chmod(0o755, path)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
def executable_script?(path)
|
|
32
|
+
File.extname(path) == ".sh" || File.open(path, &:gets).to_s.start_with?("#!")
|
|
33
|
+
end
|
|
30
34
|
end
|
|
31
35
|
end
|
data/lib/command/info.rb
CHANGED
|
@@ -106,7 +106,9 @@ module Command
|
|
|
106
106
|
@app_workloads.keys.find { |app_name| config.app_matches?(app_name, app, config.apps[app.to_sym]) }
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
# Returns boolean but mutates @missing_apps_starting_with and writes to stdout,
|
|
110
|
+
# so the method name intentionally lacks `?`.
|
|
111
|
+
def check_any_app_starts_with(app) # rubocop:disable Naming/PredicateMethod
|
|
110
112
|
if any_app_starts_with?(app)
|
|
111
113
|
false
|
|
112
114
|
else
|
data/lib/command/run.rb
CHANGED
|
@@ -265,7 +265,22 @@ module Command
|
|
|
265
265
|
|
|
266
266
|
def run_interactive
|
|
267
267
|
progress.puts("Connecting to replica '#{replica}'...\n\n")
|
|
268
|
-
|
|
268
|
+
# workload_exec returns false on non-zero exit, nil when signal-killed (e.g. Ctrl-C).
|
|
269
|
+
# Both fall through to the cleanup hint instead of the generic "non-zero status" abort.
|
|
270
|
+
success = cp.workload_exec(runner_workload, replica, location: location, container: container, command: command)
|
|
271
|
+
return if success
|
|
272
|
+
|
|
273
|
+
print_interactive_cleanup_hint
|
|
274
|
+
exit(success.nil? ? ExitCode::INTERRUPT : ExitCode::ERROR_DEFAULT)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def print_interactive_cleanup_hint
|
|
278
|
+
progress.puts(Shell.color(
|
|
279
|
+
"\nThe interactive session ended with a non-zero exit or signal from the upstream CLI. " \
|
|
280
|
+
"If the runner workload is still running, stop it with:\n " \
|
|
281
|
+
"cpflow ps:stop #{app_workload_replica_args.join(' ')} --location #{location}",
|
|
282
|
+
:yellow
|
|
283
|
+
))
|
|
269
284
|
end
|
|
270
285
|
|
|
271
286
|
def run_non_interactive
|
data/lib/command/test.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "debug"
|
|
4
|
-
|
|
5
3
|
module Command
|
|
6
4
|
class Test < Base
|
|
7
5
|
NAME = "test"
|
|
@@ -15,7 +13,7 @@ module Command
|
|
|
15
13
|
|
|
16
14
|
def call
|
|
17
15
|
# Modify this method to trigger the code you want to test.
|
|
18
|
-
#
|
|
16
|
+
# Add `require "debug"` locally if you want to use `debugger`.
|
|
19
17
|
# You can use `run_cpflow_command` to simulate a command
|
|
20
18
|
# (e.g., `run_cpflow_command("deploy-image", "-a", "my-app-name")`).
|
|
21
19
|
end
|
data/lib/core/controlplane.rb
CHANGED
|
@@ -99,7 +99,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
99
99
|
cmd << "--progress=plain" if ControlplaneApiDirect.trace
|
|
100
100
|
|
|
101
101
|
cmd.concat(docker_args)
|
|
102
|
-
build_args.each { |build_arg| cmd.
|
|
102
|
+
build_args.each { |build_arg| cmd.push("--build-arg", build_arg) }
|
|
103
103
|
cmd << docker_context
|
|
104
104
|
|
|
105
105
|
perform!(Shellwords.join(cmd))
|
|
@@ -282,7 +282,7 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
282
282
|
cmd = "cpln workload exec #{workload} #{gvc_org} --replica #{replica} --location #{location} -it"
|
|
283
283
|
cmd += " --container #{container}" if container
|
|
284
284
|
cmd += " -- #{command}"
|
|
285
|
-
perform
|
|
285
|
+
perform(cmd, output_mode: :all)
|
|
286
286
|
end
|
|
287
287
|
|
|
288
288
|
def start_cron_workload(workload, job_start_yaml, location:)
|
|
@@ -472,8 +472,19 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
472
472
|
private
|
|
473
473
|
|
|
474
474
|
def org_exists?
|
|
475
|
-
|
|
475
|
+
result = api.list_orgs
|
|
476
|
+
|
|
477
|
+
# HTTP 404 from the list-orgs endpoint returns nil in ControlplaneApiDirect.
|
|
478
|
+
# Defer scoped-token/org problems to the command's target API call, which
|
|
479
|
+
# can produce a resource-specific error.
|
|
480
|
+
return true unless result
|
|
481
|
+
|
|
482
|
+
items = result.fetch("items", [])
|
|
476
483
|
items.any? { |item| item["name"] == org }
|
|
484
|
+
rescue ControlplaneApiDirect::ForbiddenError => e
|
|
485
|
+
raise unless e.url == ControlplaneApi::LIST_ORGS_PATH
|
|
486
|
+
|
|
487
|
+
true
|
|
477
488
|
end
|
|
478
489
|
|
|
479
490
|
def ensure_org_exists!
|
|
@@ -519,7 +530,9 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
519
530
|
kernel_system_with_pid_handling(cmd)
|
|
520
531
|
end
|
|
521
532
|
|
|
522
|
-
# NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing
|
|
533
|
+
# NOTE: full analogue of Kernel.system which returns pids and saves it to child_pids for proper killing.
|
|
534
|
+
# Returns true on zero exit, false on non-zero exit, nil when the process was signal-killed.
|
|
535
|
+
# SystemCallError (e.g. cpln binary missing) propagates — startup checks ensure this is unreachable in practice.
|
|
523
536
|
def kernel_system_with_pid_handling(cmd)
|
|
524
537
|
pid = Process.spawn(cmd)
|
|
525
538
|
$child_pids << pid # rubocop:disable Style/GlobalVars
|
|
@@ -528,8 +541,6 @@ class Controlplane # rubocop:disable Metrics/ClassLength
|
|
|
528
541
|
$child_pids.delete(pid) # rubocop:disable Style/GlobalVars
|
|
529
542
|
|
|
530
543
|
status.exited? ? status.success? : nil
|
|
531
|
-
rescue SystemCallError
|
|
532
|
-
nil
|
|
533
544
|
end
|
|
534
545
|
|
|
535
546
|
def perform!(cmd, output_mode: nil, sensitive_data_pattern: nil)
|