cpflow 4.1.1 → 5.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/update-changelog.md +367 -0
  3. data/.github/workflows/claude-code-review.yml +44 -0
  4. data/.github/workflows/claude.yml +55 -0
  5. data/.gitignore +2 -0
  6. data/.overcommit.yml +43 -3
  7. data/.rubocop.yml +3 -3
  8. data/CHANGELOG.md +39 -3
  9. data/CONTRIBUTING.md +6 -0
  10. data/Gemfile +8 -7
  11. data/Gemfile.lock +93 -73
  12. data/README.md +53 -22
  13. data/cpflow.gemspec +5 -5
  14. data/docs/ai-github-flow-prompt.md +61 -0
  15. data/docs/ci-automation.md +335 -0
  16. data/docs/commands.md +70 -5
  17. data/docs/releasing.md +153 -0
  18. data/lib/command/ai_github_flow_prompt.rb +47 -0
  19. data/lib/command/base.rb +14 -0
  20. data/lib/command/cleanup_images.rb +1 -1
  21. data/lib/command/cleanup_stale_apps.rb +1 -1
  22. data/lib/command/copy_image_from_upstream.rb +14 -3
  23. data/lib/command/exists.rb +13 -2
  24. data/lib/command/generate.rb +153 -4
  25. data/lib/command/generate_github_actions.rb +170 -0
  26. data/lib/command/generator_helpers.rb +31 -0
  27. data/lib/command/github_flow_readiness.rb +37 -0
  28. data/lib/command/ps_wait.rb +5 -1
  29. data/lib/command/run.rb +4 -21
  30. data/lib/command/terraform/generate.rb +1 -0
  31. data/lib/command/version.rb +1 -0
  32. data/lib/constants/exit_code.rb +1 -0
  33. data/lib/core/config.rb +1 -1
  34. data/lib/core/controlplane.rb +13 -10
  35. data/lib/core/controlplane_api_direct.rb +25 -3
  36. data/lib/core/github_flow_readiness/checks.rb +143 -0
  37. data/lib/core/github_flow_readiness_service.rb +453 -0
  38. data/lib/core/repo_introspection.rb +118 -0
  39. data/lib/core/terraform_config/dsl.rb +1 -1
  40. data/lib/core/terraform_config/local_variable.rb +1 -1
  41. data/lib/cpflow/version.rb +1 -1
  42. data/lib/cpflow.rb +66 -3
  43. data/lib/generator_templates/Dockerfile +59 -3
  44. data/lib/generator_templates/controlplane.yml +27 -39
  45. data/lib/generator_templates/entrypoint.sh +1 -1
  46. data/lib/generator_templates/release_script.sh +23 -0
  47. data/lib/generator_templates/templates/app.yml +5 -8
  48. data/lib/generator_templates/templates/rails.yml +2 -11
  49. data/lib/generator_templates_sqlite/controlplane.yml +46 -0
  50. data/lib/generator_templates_sqlite/release_script.sh +25 -0
  51. data/lib/generator_templates_sqlite/templates/app.yml +15 -0
  52. data/lib/generator_templates_sqlite/templates/db.yml +6 -0
  53. data/lib/generator_templates_sqlite/templates/rails.yml +32 -0
  54. data/lib/generator_templates_sqlite/templates/storage.yml +6 -0
  55. data/lib/github_flow_templates/.github/actions/cpflow-build-docker-image/action.yml +131 -0
  56. data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/action.yml +24 -0
  57. data/lib/github_flow_templates/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +50 -0
  58. data/lib/github_flow_templates/.github/actions/cpflow-detect-release-phase/action.yml +62 -0
  59. data/lib/github_flow_templates/.github/actions/cpflow-setup-environment/action.yml +98 -0
  60. data/lib/github_flow_templates/.github/actions/cpflow-validate-config/action.yml +85 -0
  61. data/lib/github_flow_templates/.github/actions/cpflow-wait-for-health/action.yml +92 -0
  62. data/lib/github_flow_templates/.github/cpflow-help.md +47 -0
  63. data/lib/github_flow_templates/.github/workflows/cpflow-cleanup-stale-review-apps.yml +56 -0
  64. data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +142 -0
  65. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +445 -0
  66. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +140 -0
  67. data/lib/github_flow_templates/.github/workflows/cpflow-help-command.yml +53 -0
  68. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +490 -0
  69. data/lib/github_flow_templates/.github/workflows/cpflow-review-app-help.yml +46 -0
  70. data/rakelib/create_release.rake +662 -37
  71. data/script/check_command_docs +4 -2
  72. data/script/check_cpln_links +25 -11
  73. data/script/precommit/check_command_docs +22 -0
  74. data/script/precommit/check_cpln_links +21 -0
  75. data/script/precommit/check_trailing_newlines +68 -0
  76. data/script/precommit/get_changed_files +49 -0
  77. data/script/precommit/ruby_autofix +52 -0
  78. data/script/precommit/ruby_lint +33 -0
  79. metadata +56 -15
  80. /data/docs/{migrating.md → migrating-heroku-to-control-plane.md} +0 -0
@@ -1,23 +1,79 @@
1
- FROM ruby:3.1.2
1
+ ARG RUBY_VERSION=__RUBY_VERSION__
2
2
 
3
- RUN apt-get update
3
+ # Node and Ruby base images share the same Debian release so that glibc, libssl, and
4
+ # other system libraries line up between stages.
5
+ FROM docker.io/library/node:22-bookworm-slim AS node
6
+ FROM ruby:$RUBY_VERSION-slim-bookworm
7
+
8
+ # Yarn Classic / pnpm fallbacks for projects that haven't adopted corepack via
9
+ # `packageManager` in package.json. Override at build time (`--build-arg=YARN_CLASSIC_VERSION=...`)
10
+ # rather than editing this file so regenerating the Dockerfile keeps your pin.
11
+ ARG YARN_CLASSIC_VERSION=1.22.22
12
+ ARG PNPM_FALLBACK_VERSION=9.12.3
4
13
 
5
14
  WORKDIR /app
6
15
 
16
+ # Keep Node.js available both for asset compilation and for SSR runtimes that
17
+ # rely on ExecJS in production. Narrowed to just what the node stage actually
18
+ # ships under /usr/local so we don't drag in unused Debian libs from that image.
19
+ COPY --from=node /usr/local/bin/node /usr/local/bin/node
20
+ COPY --from=node /usr/local/bin/npm /usr/local/bin/npm
21
+ COPY --from=node /usr/local/bin/npx /usr/local/bin/npx
22
+ COPY --from=node /usr/local/bin/corepack /usr/local/bin/corepack
23
+ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
24
+ COPY --from=node /usr/local/include/node /usr/local/include/node
25
+
26
+ # Expose Corepack-managed shims so later build steps can call yarn/pnpm
27
+ # directly during asset precompilation hooks.
28
+ RUN printf '%s\n' '#!/bin/sh' 'exec corepack yarn "$@"' > /usr/bin/yarn && \
29
+ chmod +x /usr/bin/yarn && \
30
+ printf '%s\n' '#!/bin/sh' 'exec corepack pnpm "$@"' > /usr/bin/pnpm && \
31
+ chmod +x /usr/bin/pnpm
32
+
7
33
  # install ruby gems
8
34
  COPY Gemfile* ./
9
35
 
10
36
  RUN bundle config set without 'development test' && \
11
- bundle config set with 'staging production' && \
37
+ bundle config set with 'production' && \
12
38
  bundle install --jobs=3 --retry=3
13
39
 
14
40
  COPY . ./
15
41
 
42
+ # Install JavaScript dependencies only when the project actually has them.
43
+ # Pin `packageManager` in package.json to take the corepack path and avoid the
44
+ # YARN_CLASSIC_VERSION / PNPM_FALLBACK_VERSION fallbacks below; the fallbacks exist for
45
+ # projects that haven't adopted corepack and should be reviewed periodically as they go stale.
46
+ RUN if [ -f package.json ]; then \
47
+ package_manager="$(node -p "require('./package.json').packageManager || ''")"; \
48
+ if [ -f yarn.lock ]; then \
49
+ if printf '%s' "$package_manager" | grep -q '^yarn@'; then \
50
+ corepack prepare "$package_manager" --activate && \
51
+ (corepack yarn install --immutable || corepack yarn install --frozen-lockfile); \
52
+ else \
53
+ npm install -g "yarn@${YARN_CLASSIC_VERSION}" && \
54
+ (yarn install --immutable || yarn install --frozen-lockfile); \
55
+ fi; \
56
+ elif [ -f pnpm-lock.yaml ]; then \
57
+ if printf '%s' "$package_manager" | grep -q '^pnpm@'; then \
58
+ corepack prepare "$package_manager" --activate && \
59
+ corepack pnpm install --frozen-lockfile; \
60
+ else \
61
+ corepack prepare "pnpm@${PNPM_FALLBACK_VERSION}" --activate && \
62
+ corepack pnpm install --frozen-lockfile; \
63
+ fi; \
64
+ elif [ -f package-lock.json ]; then \
65
+ npm ci; \
66
+ else \
67
+ npm install; \
68
+ fi; \
69
+ fi
70
+
16
71
  ENV RAILS_ENV=production
17
72
 
18
73
  # compiling assets requires any value for ENV of SECRET_KEY_BASE
19
74
  ENV SECRET_KEY_BASE=NOT_USED_NON_BLANK
20
75
 
76
+ __ASSET_PRECOMPILE_HOOK_RUN__
21
77
  RUN rails assets:precompile
22
78
 
23
79
  # add entrypoint
@@ -1,62 +1,50 @@
1
1
  # Keys beginning with "cpln_" correspond to your settings in Control Plane.
2
+ #
3
+ # Generated baseline for a Rails app that uses PostgreSQL in production.
4
+ # Rename the app keys below if you want something other than the repo name.
2
5
 
3
- # You can opt out of allowing the use of CPLN_ORG and CPLN_APP env vars
4
- # to avoid any accidents with the wrong org / app.
5
6
  allow_org_override_by_env: true
6
7
  allow_app_override_by_env: true
7
8
 
8
9
  aliases:
9
10
  common: &common
10
- # Organization name for staging (customize to your needs).
11
- # Production apps will use a different organization, specified below, for security.
12
11
  cpln_org: my-org-staging
13
-
14
- # Example apps use only one location. Control Plane offers the ability to use multiple locations.
15
- # TODO: Allow specification of multiple locations.
16
12
  default_location: aws-us-east-2
13
+ setup_app_templates:
14
+ - app
15
+ - postgres
16
+ - rails
17
17
 
18
- # Configure the workload name used as a template for one-off scripts, like a Heroku one-off dyno.
19
18
  one_off_workload: rails
20
-
21
- # Workloads that are for the application itself and are using application Docker images.
22
- # These are updated with the new image when running the `deploy-image` command,
23
- # and are also used by the `info` and `ps:` commands in order to get all of the defined workloads.
24
- # On the other hand, if you have a workload for Redis, that would NOT use the application Docker image
25
- # and not be listed here.
26
19
  app_workloads:
27
20
  - rails
28
-
29
- # Additional "service type" workloads, using non-application Docker images.
30
- # These are only used by the `info` and `ps:` commands in order to get all of the defined workloads.
31
21
  additional_workloads:
32
22
  - postgres
33
23
 
34
- # Configure the workload name used when maintenance mode is on (defaults to "maintenance")
35
24
  maintenance_workload: maintenance
25
+ release_script: release_script.sh
36
26
 
37
- apps:
38
- my-app-staging:
39
- # Use the values from the common section above.
40
- <<: *common
41
- my-app-review:
42
- <<: *common
43
- # If `match_if_app_name_starts_with` is `true`, then use this config for app names starting with this name,
44
- # e.g., "my-app-review-pr123", "my-app-review-anything-goes", etc.
45
- match_if_app_name_starts_with: true
46
- my-app-production:
47
- <<: *common
27
+ stale_app_image_deployed_days: 5
28
+ image_retention_days: 7
48
29
 
49
- # You can also opt out of allowing the use of CPLN_ORG and CPLN_APP env vars per app.
50
- # It's recommended to leave this off for production, to avoid any accidents.
30
+ production: &production
31
+ <<: *common
51
32
  allow_org_override_by_env: false
52
33
  allow_app_override_by_env: false
53
-
54
- # Use a different organization for production.
55
34
  cpln_org: my-org-production
56
- # Allows running the command `cpflow promote-app-from-upstream -a my-app-production`
57
- # to promote the staging app to production.
58
- upstream: my-app-staging
59
- my-app-other:
35
+ upstream: __APP_PREFIX__-staging
36
+
37
+ apps:
38
+ __APP_PREFIX__-staging:
60
39
  <<: *common
61
- # You can specify a different `Dockerfile` relative to the `.controlplane/` directory (defaults to "Dockerfile").
62
- dockerfile: ../some_other/Dockerfile
40
+
41
+ __APP_PREFIX__-review:
42
+ <<: *common
43
+ match_if_app_name_starts_with: true
44
+ # Uncomment to automatically initialize and tear down review-app databases:
45
+ # hooks:
46
+ # post_creation: bundle exec rails db:prepare
47
+ # pre_deletion: bundle exec rails db:drop
48
+
49
+ __APP_PREFIX__-production:
50
+ <<: *production
@@ -4,5 +4,5 @@
4
4
  echo " -- Preparing database"
5
5
  rails db:prepare
6
6
 
7
- echo " -- Finishing entrypoint.sh, executing '$@'"
7
+ echo " -- Finishing entrypoint.sh, executing command"
8
8
  exec "$@"
@@ -0,0 +1,23 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ log() {
5
+ echo "[$(date +%Y-%m-%d:%H:%M:%S)]: $1"
6
+ }
7
+
8
+ error_exit() {
9
+ log "$1" 1>&2
10
+ exit 1
11
+ }
12
+
13
+ log "Running release_script.sh per controlplane.yml"
14
+
15
+ if [ -x ./bin/rails ]; then
16
+ log "Run DB migrations"
17
+ SECRET_KEY_BASE="${SECRET_KEY_BASE:-precompile_placeholder}" ./bin/rails db:prepare || \
18
+ error_exit "Failed to run DB migrations"
19
+ else
20
+ error_exit "./bin/rails does not exist or is not executable"
21
+ fi
22
+
23
+ log "Completed release_script.sh per controlplane.yml"
@@ -2,20 +2,17 @@
2
2
  kind: gvc
3
3
  name: {{APP_NAME}}
4
4
  spec:
5
- # For using templates for test apps, put ENV values here, stored in git repo.
6
- # Production apps will have values configured manually after app creation.
7
5
  env:
8
6
  - name: DATABASE_URL
9
- # Password does not matter because host postgres.{{APP_NAME}}.cpln.local can only be accessed
10
- # locally within CPLN GVC, and postgres running on a CPLN workload is something only for a
11
- # test app that lacks persistence.
12
7
  value: 'postgres://the_user:the_password@postgres.{{APP_NAME}}.cpln.local:5432/{{APP_NAME}}'
13
8
  - name: RAILS_ENV
14
9
  value: production
10
+ - name: RAILS_LOG_TO_STDOUT
11
+ value: "true"
15
12
  - name: RAILS_SERVE_STATIC_FILES
16
- value: 'true'
17
-
18
- # Part of standard configuration
13
+ value: "true"
14
+ - name: SECRET_KEY_BASE
15
+ value: cpln://secret/{{APP_SECRETS}}.SECRET_KEY_BASE
19
16
  staticPlacement:
20
17
  locationLinks:
21
18
  - {{APP_LOCATION_LINK}}
@@ -6,31 +6,22 @@ spec:
6
6
  type: standard
7
7
  containers:
8
8
  - name: rails
9
- # 300m is a good starting place for a test app. You can experiment with CPU configuration
10
- # once your app is running.
11
9
  cpu: 300m
12
- env:
13
- - name: LOG_LEVEL
14
- value: debug
15
- # Inherit other ENV values from GVC
16
10
  inheritEnv: true
17
11
  image: {{APP_IMAGE_LINK}}
18
- # 512 corresponds to a standard 1x dyno type
19
12
  memory: 512Mi
20
13
  ports:
21
14
  - number: 3000
22
15
  protocol: http
23
16
  defaultOptions:
24
- # Start out like this for "test apps"
25
17
  autoscaling:
26
- # Max of 1 effectively disables autoscaling, so a like a Heroku dyno count of 1
18
+ minScale: 1
27
19
  maxScale: 1
28
20
  capacityAI: false
21
+ timeoutSeconds: 60
29
22
  firewallConfig:
30
23
  external:
31
- # Default to allow public access to Rails server
32
24
  inboundAllowCIDR:
33
25
  - 0.0.0.0/0
34
- # Could configure outbound for more security
35
26
  outboundAllowCIDR:
36
27
  - 0.0.0.0/0
@@ -0,0 +1,46 @@
1
+ # Keys beginning with "cpln_" correspond to your settings in Control Plane.
2
+ #
3
+ # Generated baseline for a Rails app that uses SQLite in production. This setup
4
+ # keeps `/app/db` and `/app/storage` on persistent volumes.
5
+
6
+ allow_org_override_by_env: true
7
+ allow_app_override_by_env: true
8
+
9
+ aliases:
10
+ common: &common
11
+ cpln_org: my-org-staging
12
+ default_location: aws-us-east-2
13
+ setup_app_templates:
14
+ - app
15
+ - db
16
+ - storage
17
+ - rails
18
+
19
+ one_off_workload: rails
20
+ app_workloads:
21
+ - rails
22
+ additional_workloads: []
23
+
24
+ maintenance_workload: maintenance
25
+ release_script: release_script.sh
26
+
27
+ stale_app_image_deployed_days: 5
28
+ image_retention_days: 7
29
+
30
+ production: &production
31
+ <<: *common
32
+ allow_org_override_by_env: false
33
+ allow_app_override_by_env: false
34
+ cpln_org: my-org-production
35
+ upstream: __APP_PREFIX__-staging
36
+
37
+ apps:
38
+ __APP_PREFIX__-staging:
39
+ <<: *common
40
+
41
+ __APP_PREFIX__-review:
42
+ <<: *common
43
+ match_if_app_name_starts_with: true
44
+
45
+ __APP_PREFIX__-production:
46
+ <<: *production
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ log() {
5
+ echo "[$(date +%Y-%m-%d:%H:%M:%S)]: $1"
6
+ }
7
+
8
+ error_exit() {
9
+ log "$1" 1>&2
10
+ exit 1
11
+ }
12
+
13
+ log "Running release_script.sh per controlplane.yml"
14
+
15
+ mkdir -p db storage
16
+
17
+ if [ -x ./bin/rails ]; then
18
+ log "Run DB migrations"
19
+ SECRET_KEY_BASE="${SECRET_KEY_BASE:-precompile_placeholder}" ./bin/rails db:prepare || \
20
+ error_exit "Failed to run DB migrations"
21
+ else
22
+ error_exit "./bin/rails does not exist or is not executable"
23
+ fi
24
+
25
+ log "Completed release_script.sh per controlplane.yml"
@@ -0,0 +1,15 @@
1
+ kind: gvc
2
+ name: {{APP_NAME}}
3
+ spec:
4
+ env:
5
+ - name: RAILS_ENV
6
+ value: production
7
+ - name: RAILS_LOG_TO_STDOUT
8
+ value: "true"
9
+ - name: RAILS_SERVE_STATIC_FILES
10
+ value: "true"
11
+ - name: SECRET_KEY_BASE
12
+ value: cpln://secret/{{APP_SECRETS}}.SECRET_KEY_BASE
13
+ staticPlacement:
14
+ locationLinks:
15
+ - {{APP_LOCATION_LINK}}
@@ -0,0 +1,6 @@
1
+ kind: volumeset
2
+ name: app-db
3
+ spec:
4
+ fileSystemType: ext4
5
+ initialCapacity: 5
6
+ performanceClass: general-purpose-ssd
@@ -0,0 +1,32 @@
1
+ kind: workload
2
+ name: rails
3
+ spec:
4
+ type: standard
5
+ containers:
6
+ - name: rails
7
+ cpu: 300m
8
+ inheritEnv: true
9
+ image: {{APP_IMAGE_LINK}}
10
+ memory: 512Mi
11
+ ports:
12
+ - number: 3000
13
+ protocol: http
14
+ volumes:
15
+ - path: /app/db
16
+ recoveryPolicy: retain
17
+ uri: cpln://volumeset/app-db
18
+ - path: /app/storage
19
+ recoveryPolicy: retain
20
+ uri: cpln://volumeset/app-storage
21
+ defaultOptions:
22
+ autoscaling:
23
+ minScale: 1
24
+ maxScale: 1
25
+ capacityAI: false
26
+ timeoutSeconds: 60
27
+ firewallConfig:
28
+ external:
29
+ inboundAllowCIDR:
30
+ - 0.0.0.0/0
31
+ outboundAllowCIDR:
32
+ - 0.0.0.0/0
@@ -0,0 +1,6 @@
1
+ kind: volumeset
2
+ name: app-storage
3
+ spec:
4
+ fileSystemType: ext4
5
+ initialCapacity: 10
6
+ performanceClass: general-purpose-ssd
@@ -0,0 +1,131 @@
1
+ name: Build Docker Image
2
+ description: Builds and pushes the app image for a Control Plane workload
3
+
4
+ inputs:
5
+ app_name:
6
+ description: Name of the application
7
+ required: true
8
+ org:
9
+ description: Control Plane organization name
10
+ required: true
11
+ commit:
12
+ description: Commit SHA to tag the image with
13
+ required: true
14
+ pr_number:
15
+ description: Pull request number for status messaging
16
+ required: false
17
+ docker_build_extra_args:
18
+ description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar.
19
+ required: false
20
+ docker_build_ssh_key:
21
+ description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh
22
+ required: false
23
+ docker_build_ssh_known_hosts:
24
+ description: Optional SSH known_hosts entries used with docker_build_ssh_key. Defaults to pinned GitHub.com host keys.
25
+ required: false
26
+ working_directory:
27
+ description: Directory containing the app .controlplane config and Docker build context
28
+ required: false
29
+ default: "."
30
+
31
+ runs:
32
+ using: composite
33
+ steps:
34
+ # Keep SSH key handling in a dedicated step so DOCKER_BUILD_SSH_KEY is never present
35
+ # in the main build step's environment. ACTIONS_STEP_DEBUG=true dumps env before any
36
+ # command runs, so keeping the key out of env there avoids even admin-triggered exposure.
37
+ - name: Prepare SSH agent for Docker build
38
+ if: ${{ inputs.docker_build_ssh_key != '' }}
39
+ shell: bash
40
+ env:
41
+ # Pass the key via env so the file write is a single printf call rather than a
42
+ # heredoc with a fixed terminator (a heredoc would silently truncate the key if
43
+ # any line of the key value happened to match the terminator). Scope is still
44
+ # this step only — the build step below does not receive DOCKER_BUILD_SSH_KEY.
45
+ DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }}
46
+ DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }}
47
+ run: |
48
+ set -euo pipefail
49
+
50
+ umask 077
51
+ mkdir -p ~/.ssh
52
+ chmod 700 ~/.ssh
53
+
54
+ if [[ -n "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" ]]; then
55
+ printf '%s\n' "${DOCKER_BUILD_SSH_KNOWN_HOSTS}" > ~/.ssh/known_hosts
56
+ else
57
+ printf '%s\n' \
58
+ 'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl' \
59
+ 'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' \
60
+ 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=' \
61
+ > ~/.ssh/known_hosts
62
+ fi
63
+ chmod 600 ~/.ssh/known_hosts
64
+
65
+ printf '%s\n' "${DOCKER_BUILD_SSH_KEY}" > ~/.ssh/cpflow_build_key
66
+ chmod 600 ~/.ssh/cpflow_build_key
67
+
68
+ - name: Build Docker image
69
+ shell: bash
70
+ env:
71
+ APP_NAME: ${{ inputs.app_name }}
72
+ COMMIT_SHA: ${{ inputs.commit }}
73
+ CONTROL_PLANE_ORG: ${{ inputs.org }}
74
+ DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }}
75
+ PR_NUMBER: ${{ inputs.pr_number }}
76
+ WORKING_DIRECTORY: ${{ inputs.working_directory }}
77
+ run: |
78
+ set -euo pipefail
79
+
80
+ PR_INFO=""
81
+ docker_build_args=()
82
+ ssh_agent_started=false
83
+ build_ssh_prepped=false
84
+
85
+ cleanup_build_ssh() {
86
+ if [[ "${ssh_agent_started}" == "true" ]]; then
87
+ ssh-agent -k >/dev/null || true
88
+ fi
89
+ rm -f "${HOME}/.ssh/cpflow_build_key"
90
+ # Only remove known_hosts if this action's prep step wrote it. On self-hosted
91
+ # or reused runners we must not touch a user-managed file we did not create,
92
+ # so the flag is set inside the same prep-detection branch below.
93
+ if [[ "${build_ssh_prepped}" == "true" ]]; then
94
+ rm -f "${HOME}/.ssh/known_hosts"
95
+ fi
96
+ }
97
+ trap cleanup_build_ssh EXIT
98
+ cd "${WORKING_DIRECTORY}"
99
+
100
+ if [[ -n "${PR_NUMBER}" ]]; then
101
+ PR_INFO=" for PR #${PR_NUMBER}"
102
+ fi
103
+
104
+ if [[ -n "${DOCKER_BUILD_EXTRA_ARGS}" ]]; then
105
+ while IFS= read -r arg; do
106
+ arg="${arg%$'\r'}"
107
+ [[ -n "${arg}" ]] || continue
108
+
109
+ if [[ "${arg}" =~ [[:space:]] ]]; then
110
+ echo "docker_build_extra_args entries must be single docker-build tokens. " \
111
+ "Use key=value forms like --build-arg=FOO=bar." >&2
112
+ exit 1
113
+ fi
114
+
115
+ docker_build_args+=("${arg}")
116
+ done <<< "${DOCKER_BUILD_EXTRA_ARGS}"
117
+ fi
118
+
119
+ if [[ -f "${HOME}/.ssh/cpflow_build_key" ]]; then
120
+ # Mark prep-step ownership so cleanup_build_ssh only removes known_hosts
121
+ # when this action wrote it (see trap above).
122
+ build_ssh_prepped=true
123
+ eval "$(ssh-agent -s)"
124
+ ssh_agent_started=true
125
+ ssh-add "${HOME}/.ssh/cpflow_build_key"
126
+ docker_build_args+=("--ssh=default")
127
+ fi
128
+
129
+ echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT_SHA})..."
130
+ cpflow build-image -a "${APP_NAME}" --commit="${COMMIT_SHA}" --org="${CONTROL_PLANE_ORG}" "${docker_build_args[@]}"
131
+ echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT_SHA})"
@@ -0,0 +1,24 @@
1
+ name: Delete Control Plane App
2
+ description: Deletes a Control Plane app and all associated resources
3
+
4
+ inputs:
5
+ app_name:
6
+ description: Name of the application to delete
7
+ required: true
8
+ cpln_org:
9
+ description: Control Plane organization name
10
+ required: true
11
+ review_app_prefix:
12
+ description: Prefix used for review app names
13
+ required: true
14
+
15
+ runs:
16
+ using: composite
17
+ steps:
18
+ - name: Delete application
19
+ shell: bash
20
+ run: ${{ github.action_path }}/delete-app.sh
21
+ env:
22
+ APP_NAME: ${{ inputs.app_name }}
23
+ CPLN_ORG: ${{ inputs.cpln_org }}
24
+ REVIEW_APP_PREFIX: ${{ inputs.review_app_prefix }}
@@ -0,0 +1,50 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ : "${APP_NAME:?APP_NAME environment variable is required}"
6
+ : "${CPLN_ORG:?CPLN_ORG environment variable is required}"
7
+ : "${REVIEW_APP_PREFIX:?REVIEW_APP_PREFIX environment variable is required}"
8
+
9
+ expected_prefix="${REVIEW_APP_PREFIX}-"
10
+ if [[ "$APP_NAME" != "${expected_prefix}"* ]]; then
11
+ echo "❌ ERROR: refusing to delete an app outside the review app prefix" >&2
12
+ echo "App name: $APP_NAME" >&2
13
+ echo "Expected prefix: ${expected_prefix}" >&2
14
+ exit 1
15
+ fi
16
+
17
+ echo "🔍 Checking if application exists: $APP_NAME"
18
+ exists_output=""
19
+ set +e
20
+ exists_output="$(cpflow exists -a "$APP_NAME" --org "$CPLN_ORG" 2>&1)"
21
+ exists_status=$?
22
+ set -e
23
+
24
+ case "$exists_status" in
25
+ 0)
26
+ ;;
27
+ 3)
28
+ if [[ -n "$exists_output" ]]; then
29
+ printf '%s\n' "$exists_output"
30
+ fi
31
+ echo "⚠️ Application does not exist: $APP_NAME"
32
+ exit 0
33
+ ;;
34
+ *)
35
+ echo "❌ ERROR: failed to determine whether application exists: $APP_NAME" >&2
36
+ if [[ -n "$exists_output" ]]; then
37
+ printf '%s\n' "$exists_output" >&2
38
+ fi
39
+ exit "$exists_status"
40
+ ;;
41
+ esac
42
+
43
+ if [[ -n "$exists_output" ]]; then
44
+ printf '%s\n' "$exists_output"
45
+ fi
46
+
47
+ echo "🗑️ Deleting application: $APP_NAME"
48
+ cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes
49
+
50
+ echo "✅ Successfully deleted application: $APP_NAME"
@@ -0,0 +1,62 @@
1
+ name: Detect release phase support
2
+ description: >-
3
+ Inspects .controlplane/controlplane.yml for an app and emits `flag=--run-release-phase`
4
+ when a `release_script:` is configured. Outputs an empty `flag` otherwise.
5
+
6
+ inputs:
7
+ app_name:
8
+ description: cpflow app name to inspect
9
+ required: true
10
+ working_directory:
11
+ description: Directory containing .controlplane/controlplane.yml
12
+ required: false
13
+ default: "."
14
+
15
+ outputs:
16
+ flag:
17
+ description: Either `--run-release-phase` or empty
18
+ value: ${{ steps.detect.outputs.flag }}
19
+
20
+ runs:
21
+ using: composite
22
+ steps:
23
+ - name: Detect release phase support
24
+ id: detect
25
+ shell: bash
26
+ env:
27
+ APP_NAME: ${{ inputs.app_name }}
28
+ WORKING_DIRECTORY: ${{ inputs.working_directory }}
29
+ run: |
30
+ set -euo pipefail
31
+ cd "${WORKING_DIRECTORY}"
32
+
33
+ release_script="$(ruby - "${APP_NAME}" <<'RUBY'
34
+ require "yaml"
35
+
36
+ app_name = ARGV.fetch(0)
37
+ data = YAML.safe_load(File.read(".controlplane/controlplane.yml"), aliases: true)
38
+ apps = data["apps"] || {}
39
+ app_config = apps[app_name]
40
+
41
+ unless app_config
42
+ app_config = apps.find do |name, config|
43
+ config.is_a?(Hash) &&
44
+ config["match_if_app_name_starts_with"] &&
45
+ app_name.start_with?(name)
46
+ end&.last
47
+ end
48
+
49
+ unless app_config.is_a?(Hash)
50
+ warn "Error: app '#{app_name}' is not defined under `apps:` in `.controlplane/controlplane.yml`."
51
+ exit 1
52
+ end
53
+
54
+ puts app_config["release_script"].to_s
55
+ RUBY
56
+ )"
57
+
58
+ if [[ -n "${release_script}" ]]; then
59
+ echo "flag=--run-release-phase" >> "$GITHUB_OUTPUT"
60
+ else
61
+ echo "flag=" >> "$GITHUB_OUTPUT"
62
+ fi