cpflow 6.0.0.rc.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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/agent-workflow.yml +16 -38
  3. data/.agents/bin/README.md +7 -8
  4. data/.agents/legacy-workflow-policy.yml +11 -0
  5. data/.agents/shaka.md +20 -0
  6. data/.agents/trusted-actions.yml +12 -0
  7. data/.coderabbit.yaml +1 -0
  8. data/.github/actions/cpflow-build-docker-image/action.yml +153 -22
  9. data/.github/actions/cpflow-delete-control-plane-app/delete-app.sh +3 -0
  10. data/.github/actions/cpflow-preview-promotion/action.yml +82 -0
  11. data/.github/actions/cpflow-preview-promotion/preview-promotion.sh +90 -0
  12. data/.github/workflows/cpflow-deploy-review-app.yml +13 -1
  13. data/.github/workflows/cpflow-deploy-staging.yml +12 -1
  14. data/.github/workflows/cpflow-promote-staging-to-production.yml +128 -9
  15. data/.github/workflows/rspec.yml +24 -0
  16. data/AGENTS.md +18 -3
  17. data/CHANGELOG.md +18 -3
  18. data/CONTRIBUTING.md +1 -1
  19. data/Gemfile.lock +1 -1
  20. data/README.md +5 -2
  21. data/docs/ai-github-flow-prompt.md +2 -2
  22. data/docs/ci-automation.md +178 -14
  23. data/docs/commands.md +26 -13
  24. data/docs/postgres-review-apps.md +90 -0
  25. data/docs/postgres.md +233 -375
  26. data/docs/secrets-and-env-values.md +42 -0
  27. data/docs/sidebars.ts +1 -0
  28. data/lib/command/ai_github_flow_prompt.rb +2 -2
  29. data/lib/command/apply_template.rb +36 -4
  30. data/lib/command/base.rb +56 -3
  31. data/lib/command/delete.rb +174 -4
  32. data/lib/command/deploy_image.rb +6 -0
  33. data/lib/command/generate.rb +247 -3
  34. data/lib/command/generate_github_actions.rb +7 -1
  35. data/lib/command/setup_app.rb +92 -10
  36. data/lib/command/update_github_actions.rb +81 -43
  37. data/lib/core/config.rb +73 -0
  38. data/lib/core/controlplane.rb +22 -2
  39. data/lib/core/controlplane_api.rb +28 -2
  40. data/lib/core/controlplane_api_direct.rb +17 -0
  41. data/lib/core/disposable_postgres_helpers.rb +167 -0
  42. data/lib/core/doctor_service.rb +7 -0
  43. data/lib/core/github_flow_readiness/checks.rb +2 -1
  44. data/lib/core/github_flow_readiness_service.rb +4 -0
  45. data/lib/core/repo_introspection.rb +112 -2
  46. data/lib/cpflow/version.rb +1 -1
  47. data/lib/generator_templates/.dockerignore +16 -0
  48. data/lib/generator_templates/Dockerfile +9 -5
  49. data/lib/generator_templates/entrypoint.sh +2 -0
  50. data/lib/generator_templates/templates/postgres.yml +9 -0
  51. data/lib/generator_templates_sqlite/controlplane.yml +2 -1
  52. data/lib/generator_templates_sqlite/release_script.sh +1 -1
  53. data/lib/generator_templates_sqlite/templates/rails.yml +2 -1
  54. data/lib/github_flow_templates/.github/cpflow-help.md +39 -12
  55. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +1 -0
  56. data/lib/github_flow_templates/.github/workflows/cpflow-deploy-staging.yml +1 -0
  57. data/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +128 -9
  58. data/lib/github_flow_templates/bin/pin-cpflow-github-ref +37 -13
  59. data/lib/github_flow_templates/bin/test-cpflow-github-flow +62 -43
  60. metadata +11 -3
@@ -12,7 +12,7 @@ error_exit() {
12
12
 
13
13
  log "Running release_script.sh per controlplane.yml"
14
14
 
15
- mkdir -p db storage
15
+ mkdir -p data storage
16
16
 
17
17
  if [ -x ./bin/rails ]; then
18
18
  log "Run DB migrations"
@@ -16,7 +16,8 @@ spec:
16
16
  # See https://www.shakacode.com/control-plane-flow/docs/thruster/ for details.
17
17
  protocol: http
18
18
  volumes:
19
- - path: /app/db
19
+ # Keep migrations and schema from the image visible under /app/db.
20
+ - path: /app/data
20
21
  recoveryPolicy: retain
21
22
  uri: cpln://volumeset/app-db
22
23
  - path: /app/storage
@@ -132,21 +132,30 @@ production org, using production-only secrets and values.
132
132
 
133
133
  ## Version Locking
134
134
 
135
- Generated wrappers pin Control Plane Flow with a release tag, for example
136
- `__CPFLOW_GITHUB_ACTIONS_REF__`. Reusable review-app, staging, cleanup, and
137
- helper workflows pin the tag in their `uses:` ref. Production promotion pins
138
- the same tag in the `Checkout control-plane-flow actions` step so the
135
+ Freshly generated wrappers start with a Control Plane Flow release tag, for
136
+ example `__CPFLOW_GITHUB_ACTIONS_REF__`. Before review and merge, use the pin
137
+ helper below to replace that tag with its immutable commit SHA and retain the
138
+ reviewed tag in a readable comment. Reusable review-app, staging, cleanup, and
139
+ helper workflows then use that SHA. Production promotion uses the same SHA and
140
+ tag in the `Checkout control-plane-flow actions` step so the
139
141
  caller-owned job can keep `environment: production` and receive production
140
142
  environment secrets directly.
141
143
 
142
144
  Leave `CPFLOW_VERSION` unset so the workflow builds cpflow from the same
143
- checked-out upstream source. If you set `CPFLOW_VERSION`, it must match the
144
- release tag your wrappers are pinned to: a `CPFLOW_VERSION=__CPFLOW_MINOR_SERIES__` runtime
145
- override goes with a wrapper pinned to `uses: ...@v__CPFLOW_MINOR_SERIES__` (substitute the
146
- release you pinned above).
145
+ checked-out upstream source. `CPFLOW_VERSION` is supported only while the
146
+ wrapper itself uses an actual release-tag ref; it must match that tag. Keep it
147
+ unset after replacing the wrapper ref with an immutable SHA because the
148
+ checked-out source already determines the runtime version.
147
149
 
148
- After updating the `cpflow` gem in this repo, update the generated wrappers in
149
- the same PR:
150
+ If an existing `bin/test-cpflow-github-flow` differs from cpflow's generated
151
+ validator, first preserve its downstream checks in executable
152
+ `bin/test-cpflow-github-flow-custom`, then remove or rename the old generated
153
+ validator before retrying the update.
154
+
155
+ After updating the `cpflow` gem in this repo, refresh the generated local
156
+ actions and validation helpers in the same PR. The default command
157
+ preserves every top-level workflow, including its refs, triggers, permissions, and
158
+ downstream customizations:
150
159
 
151
160
  ```sh
152
161
  cpflow update-github-actions
@@ -160,12 +169,30 @@ bundle exec cpflow update-github-actions
160
169
  bin/test-cpflow-github-flow bundle exec cpflow
161
170
  ```
162
171
 
172
+ Replace workflow wrappers only after explicitly selecting and reviewing them:
173
+
174
+ ```sh
175
+ cpflow update-github-actions --workflows FILE...
176
+ ```
177
+
178
+ Reapply required downstream customizations after a selected replacement, then
179
+ pin and validate the complete flow.
180
+
181
+ For a reviewed release, resolve its tag to the exact commit SHA, then pin and
182
+ validate that immutable source:
183
+
184
+ ```sh
185
+ bin/pin-cpflow-github-ref --version vX.Y.Z <40-character-control-plane-flow-commit-sha>
186
+ bin/test-cpflow-github-flow
187
+ ```
188
+
163
189
  Do not leave downstream apps pinned to a moving branch such as `main`. For a
164
190
  short-lived test of an unreleased upstream PR, pin to a full 40-character commit
165
- SHA and leave `CPFLOW_VERSION` unset:
191
+ SHA, use the upstream source's current in-progress version tag as the readable
192
+ `--version` label, and leave `CPFLOW_VERSION` unset:
166
193
 
167
194
  ```sh
168
- bin/pin-cpflow-github-ref <40-character-control-plane-flow-commit-sha>
195
+ bin/pin-cpflow-github-ref --version vX.Y.Z <40-character-control-plane-flow-commit-sha>
169
196
  bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/bin/cpflow
170
197
  ```
171
198
 
@@ -42,4 +42,5 @@ jobs:
42
42
  uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@__CPFLOW_GITHUB_ACTIONS_REF__
43
43
  secrets:
44
44
  CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
45
+ REVIEW_APP_DOCKER_BUILD_SECRETS: ${{ secrets.REVIEW_APP_DOCKER_BUILD_SECRETS }}
45
46
  DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }}
@@ -21,4 +21,5 @@ jobs:
21
21
  staging_app_branch_default: "__STAGING_BRANCH_DEFAULT__"
22
22
  secrets:
23
23
  CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }}
24
+ DOCKER_BUILD_SECRETS: ${{ secrets.DOCKER_BUILD_SECRETS }}
24
25
  DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }}
@@ -46,18 +46,84 @@ concurrency:
46
46
  cancel-in-progress: false
47
47
 
48
48
  jobs:
49
+ preview-promotion:
50
+ if: github.event.inputs.confirm_promotion == 'promote'
51
+ runs-on: ubuntu-latest
52
+ timeout-minutes: 10
53
+ # No production Environment or production token: this finishes before approval.
54
+ outputs:
55
+ primary_workload: ${{ steps.preview.outputs.primary_workload }}
56
+ staging_image: ${{ steps.preview.outputs.staging_image }}
57
+ production_commit: ${{ steps.preview.outputs.production_commit }}
58
+ steps:
59
+ - name: Checkout repository
60
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
61
+ with:
62
+ persist-credentials: false
63
+
64
+ - name: Checkout control-plane-flow actions
65
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
66
+ with:
67
+ repository: shakacode/control-plane-flow
68
+ ref: __CPFLOW_GITHUB_ACTIONS_REF__
69
+ path: .cpflow
70
+ persist-credentials: false
71
+
72
+ - name: Normalize staging org
73
+ id: staging-org
74
+ env:
75
+ STAGING_ORG: ${{ vars.CPLN_ORG_STAGING }}
76
+ shell: bash
77
+ run: |
78
+ set -euo pipefail
79
+ org="${STAGING_ORG#"${STAGING_ORG%%[![:space:]]*}"}"
80
+ org="${org%"${org##*[![:space:]]}"}"
81
+ if ! [[ "$org" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then
82
+ echo "::error::CPLN_ORG_STAGING must be a valid Control Plane org name."
83
+ exit 1
84
+ fi
85
+ echo "org=$org" >> "$GITHUB_OUTPUT"
86
+
87
+ - name: Setup staging environment
88
+ uses: ./.github/actions/cpflow-setup-environment
89
+ with:
90
+ token: ${{ secrets.CPLN_TOKEN_STAGING }}
91
+ org: ${{ steps.staging-org.outputs.org }}
92
+ working_directory: .cpflow
93
+ cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }}
94
+ cpflow_version: ${{ vars.CPFLOW_VERSION }}
95
+ # The setup action validates CPFLOW_VERSION against this full workflow ref.
96
+ control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@__CPFLOW_GITHUB_ACTIONS_REF__
97
+
98
+ - name: Preview commits before production approval
99
+ id: preview
100
+ uses: ./.github/actions/cpflow-preview-promotion
101
+ env:
102
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
103
+ GH_REPO: ${{ github.repository }}
104
+ CPLN_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }}
105
+ with:
106
+ staging_app_name: ${{ vars.STAGING_APP_NAME }}
107
+ staging_org: ${{ steps.staging-org.outputs.org }}
108
+ primary_workload: ${{ vars.PRIMARY_WORKLOAD }}
109
+
49
110
  promote-to-production:
111
+ needs: preview-promotion
50
112
  if: github.event.inputs.confirm_promotion == 'promote'
51
113
  runs-on: ubuntu-latest
52
114
  # This normal caller-repo job declares the protected production Environment
53
115
  # directly, so GitHub exposes environment secrets in this job after the
54
116
  # environment gate. Do not move production promotion back behind a
55
117
  # cross-repo reusable workflow; environment secrets are not available there.
56
- environment: production
118
+ # The URL gives the deployment a "View deployment" link on GitHub.
119
+ environment:
120
+ name: production
121
+ url: ${{ steps.health-check.outputs.endpoint }}
57
122
  timeout-minutes: 45
58
123
  outputs:
59
124
  staging_app_name: ${{ steps.release-context.outputs.staging_app_name }}
60
125
  production_app_name: ${{ steps.release-context.outputs.production_app_name }}
126
+ staging_commit: ${{ steps.copy-image.outputs.staging_commit }}
61
127
 
62
128
  steps:
63
129
  - name: Checkout repository
@@ -440,6 +506,33 @@ jobs:
440
506
 
441
507
  echo "image=${staging_image}" >> "$GITHUB_OUTPUT"
442
508
 
509
+ - name: Verify approved promotion snapshot
510
+ id: verify-preview
511
+ env:
512
+ PREVIEW_WORKLOAD: ${{ needs.preview-promotion.outputs.primary_workload }}
513
+ CURRENT_PRIMARY_WORKLOAD: ${{ steps.workloads.outputs.primary }}
514
+ PREVIEW_IMAGE: ${{ needs.preview-promotion.outputs.staging_image }}
515
+ PREVIEW_PRODUCTION_COMMIT: ${{ needs.preview-promotion.outputs.production_commit }}
516
+ CURRENT_STAGING_IMAGE: ${{ steps.staging-image.outputs.image }}
517
+ CURRENT_PRODUCTION_IMAGE: ${{ steps.capture-current.outputs.current_image }}
518
+ shell: bash
519
+ run: |
520
+ set -euo pipefail
521
+ if [[ -z "$PREVIEW_WORKLOAD" || "$PREVIEW_WORKLOAD" != "$CURRENT_PRIMARY_WORKLOAD" ]]; then
522
+ echo "::error::The production primary workload differs from the preview. Set PRIMARY_WORKLOAD as a repository variable shared by both jobs and rerun."
523
+ exit 1
524
+ fi
525
+ if [[ -z "$PREVIEW_IMAGE" || "$CURRENT_STAGING_IMAGE" != "$PREVIEW_IMAGE" ]]; then
526
+ echo "::error::Staging changed since the preview. Rerun promotion and review the new comparison."
527
+ exit 1
528
+ fi
529
+ production_tag="${CURRENT_PRODUCTION_IMAGE%%@*}"
530
+ production_commit="${production_tag##*_}"
531
+ if [[ -n "$PREVIEW_PRODUCTION_COMMIT" && "$production_commit" != "$PREVIEW_PRODUCTION_COMMIT" ]]; then
532
+ echo "::error::Live production does not match its recorded release. Reconcile the production release record, then rerun promotion."
533
+ exit 1
534
+ fi
535
+
443
536
  - name: Set up Docker Buildx
444
537
  uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
445
538
 
@@ -476,22 +569,26 @@ jobs:
476
569
  exit 1
477
570
  fi
478
571
 
479
- if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${staging_image}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then
572
+ staging_image_without_digest="${staging_image%%@*}"
573
+ if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${staging_image_without_digest}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then
480
574
  echo "::error::Staging image '${STAGING_IMAGE}' was not found in org '${CPLN_ORG_STAGING}'; aborting promotion."
481
575
  exit 1
482
576
  fi
483
577
 
484
578
  staging_tag=""
485
- if [[ "${staging_image}" == *@* ]]; then
486
- staging_tag="${staging_image##*@}"
487
- elif [[ "${staging_image}" == *:* ]]; then
488
- staging_tag="${staging_image##*:}"
579
+ if [[ "${staging_image_without_digest}" == *:* ]]; then
580
+ staging_tag="${staging_image_without_digest##*:}"
489
581
  fi
490
582
  staging_commit=""
491
583
  if [[ "${staging_tag}" == *_* ]]; then
492
584
  staging_commit="${staging_tag##*_}"
493
585
  else
494
- echo "::warning::Staging image '${staging_image}' did not include a '_<commit>' suffix; production image tag will omit the commit suffix."
586
+ echo "::error::Staging image '${staging_image}' does not include the commit suffix required for a traceable production release."
587
+ exit 1
588
+ fi
589
+ if ! [[ "${staging_commit}" =~ ^[0-9a-f]{40}$ ]]; then
590
+ echo "::error::Staging image '${staging_image}' has an invalid commit suffix; expected a full lowercase Git commit SHA."
591
+ exit 1
495
592
  fi
496
593
 
497
594
  # The workflow-level concurrency group serializes this sequence so two
@@ -566,8 +663,10 @@ jobs:
566
663
  fi
567
664
 
568
665
  echo "image=${production_image}" >> "$GITHUB_OUTPUT"
666
+ echo "staging_commit=${staging_commit}" >> "$GITHUB_OUTPUT"
569
667
 
570
668
  - name: Deploy image to production
669
+ id: deploy-production
571
670
  env:
572
671
  PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
573
672
  CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }}
@@ -576,6 +675,16 @@ jobs:
576
675
  run: |
577
676
  set -euo pipefail
578
677
 
678
+ deployment_marker_dir="$(mktemp -d)"
679
+ export CPFLOW_DEPLOYMENT_STARTED_FILE="${deployment_marker_dir}/started"
680
+ record_deployment_state() {
681
+ if [[ -f "${CPFLOW_DEPLOYMENT_STARTED_FILE}" ]]; then
682
+ echo "deployment_started=true" >> "${GITHUB_OUTPUT}"
683
+ fi
684
+ rm -rf "${deployment_marker_dir}"
685
+ }
686
+ trap record_deployment_state EXIT
687
+
579
688
  deploy_args=(-a "${PRODUCTION_APP_NAME}")
580
689
  if [[ -n "${RELEASE_PHASE_FLAG}" ]]; then
581
690
  deploy_args+=("${RELEASE_PHASE_FLAG}")
@@ -599,7 +708,11 @@ jobs:
599
708
  accepted_statuses: ${{ env.HEALTH_CHECK_ACCEPTED_STATUSES }}
600
709
 
601
710
  - name: Roll back on failure
602
- if: failure() && steps.capture-current.outcome == 'success'
711
+ if: >-
712
+ failure() &&
713
+ steps.deploy-production.outputs.deployment_started == 'true' &&
714
+ steps.capture-current.outputs.rollback_state != '' &&
715
+ steps.capture-current.outputs.rollback_state != '{}'
603
716
  env:
604
717
  ROLLBACK_STATE: ${{ steps.capture-current.outputs.rollback_state }}
605
718
  PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
@@ -661,7 +774,11 @@ jobs:
661
774
  fi
662
775
 
663
776
  - name: Wait for rollback readiness
664
- if: failure() && steps.capture-current.outcome == 'success'
777
+ if: >-
778
+ failure() &&
779
+ steps.deploy-production.outputs.deployment_started == 'true' &&
780
+ steps.capture-current.outputs.rollback_state != '' &&
781
+ steps.capture-current.outputs.rollback_state != '{}'
665
782
  env:
666
783
  ROLLBACK_STATE: ${{ steps.capture-current.outputs.rollback_state }}
667
784
  PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
@@ -770,6 +887,7 @@ jobs:
770
887
  GITHUB_RUN_ID: ${{ github.run_id }}
771
888
  STAGING_APP_NAME: ${{ needs.promote-to-production.outputs.staging_app_name }}
772
889
  PRODUCTION_APP_NAME: ${{ needs.promote-to-production.outputs.production_app_name }}
890
+ STAGING_COMMIT: ${{ needs.promote-to-production.outputs.staging_commit }}
773
891
  shell: bash
774
892
  run: |
775
893
  set -euo pipefail
@@ -779,5 +897,6 @@ jobs:
779
897
  release_tag="production-${release_date}-${timestamp}-${GITHUB_RUN_ID}"
780
898
 
781
899
  gh release create "${release_tag}" \
900
+ --target "${STAGING_COMMIT}" \
782
901
  --title "Production Release ${release_date} ${timestamp}" \
783
902
  --notes "Promoted ${STAGING_APP_NAME} to ${PRODUCTION_APP_NAME} on ${release_date} at ${timestamp}."
@@ -2,19 +2,28 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "pathname"
5
+ require "optparse"
5
6
 
6
7
  USAGE = <<~USAGE
7
- Usage: bin/pin-cpflow-github-ref [--allow-moving-ref] <control-plane-flow-ref>
8
+ Usage: bin/pin-cpflow-github-ref [--version TAG] [--allow-moving-ref] <control-plane-flow-ref>
8
9
 
9
10
  Use a release tag for normal operation, e.g. v5.0.0.
10
11
  Use a full 40-character commit SHA for temporary unreleased upstream testing.
12
+ --version TAG supplies the reviewed v-prefixed version/tag for SHA comments
13
+ (required for a SHA; no default). Verify the tag/version against upstream first.
14
+ This helper does not resolve tags over the network or verify tag-to-SHA identity.
11
15
  This updates generated reusable-workflow `uses:` refs plus the production
12
16
  workflow's pinned control-plane-flow checkout and setup validation ref.
13
17
  Regenerate from the cpflow gem when templates changed.
14
18
  Use --allow-moving-ref only for short-lived local branch/ref experiments.
19
+ Use -h or --help to show this help.
15
20
  USAGE
16
21
 
17
- ALLOWED_OPTIONS = ["--allow-moving-ref"].freeze
22
+ if ARGV.include?("--help") || ARGV.include?("-h")
23
+ puts USAGE
24
+ exit 0
25
+ end
26
+
18
27
  FULL_COMMIT_SHA = /\A[0-9a-f]{40}\z/i
19
28
  RELEASE_TAG = /\Av\d+\.\d+\.\d+(?:[-.][0-9A-Za-z][0-9A-Za-z.-]*)?\z/
20
29
  PRODUCTION_WORKFLOW_REF = "shakacode/control-plane-flow/.github/workflows/" \
@@ -25,14 +34,19 @@ CPFLOW_CHECKOUT_REF_PATTERN = %r{
25
34
  ^\s+repository:\s+shakacode/control-plane-flow\s*\n
26
35
  (?:(?!^\s*-\s+name:).)*?
27
36
  ^\s+ref:\s+)
28
- [^\s]+
37
+ [^\s\#]+ [\t\ ]* (?:\#[^\n]*)?
29
38
  }mx
30
39
 
31
- options, positional = ARGV.partition { |arg| arg.start_with?("--") }
32
- unknown_options = options - ALLOWED_OPTIONS
33
-
34
- unless unknown_options.empty?
35
- warn "Unknown option(s): #{unknown_options.join(', ')}"
40
+ version = nil
41
+ allow_moving_ref = false
42
+ parser = OptionParser.new do |options|
43
+ options.on("--version TAG") { |value| version = value }
44
+ options.on("--allow-moving-ref") { allow_moving_ref = true }
45
+ end
46
+ begin
47
+ positional = parser.parse(ARGV)
48
+ rescue OptionParser::ParseError => e
49
+ warn e.message
36
50
  warn USAGE
37
51
  exit 1
38
52
  end
@@ -43,13 +57,21 @@ unless positional.length == 1
43
57
  end
44
58
 
45
59
  ref = positional.first
46
- allow_moving_ref = options.include?("--allow-moving-ref")
47
60
 
48
61
  unless ref.match?(%r{\A[0-9A-Za-z._/-]+\z})
49
62
  warn "Ref contains unsupported characters: #{ref.inspect}"
50
63
  exit 1
51
64
  end
52
65
 
66
+ if (version && !version.match?(RELEASE_TAG)) || (ref.match?(FULL_COMMIT_SHA) && !version)
67
+ warn "A SHA pin requires --version TAG with a reviewed v-prefixed version/tag (for example v6.0.0.rc.0)."
68
+ warn USAGE
69
+ exit 1
70
+ end
71
+
72
+ ref = ref.downcase if ref.match?(FULL_COMMIT_SHA)
73
+ annotated_ref = ref.match?(FULL_COMMIT_SHA) ? "#{ref} # #{version}" : ref
74
+
53
75
  unless ref.match?(FULL_COMMIT_SHA) || ref.match?(RELEASE_TAG) || allow_moving_ref
54
76
  warn "Ref must be a full 40-character commit SHA or a v-prefixed release tag: #{ref.inspect}"
55
77
  warn "Use --allow-moving-ref only for a short-lived branch/ref experiment."
@@ -68,11 +90,13 @@ changed = []
68
90
  workflow_paths.each do |path|
69
91
  text = File.read(path)
70
92
  production_setup_ref_pattern =
71
- /(control_plane_flow_ref:\s+#{Regexp.escape(PRODUCTION_WORKFLOW_REF)}@)[^\s]+/
93
+ /(control_plane_flow_ref:\s+#{Regexp.escape(PRODUCTION_WORKFLOW_REF)}@)[^\s#]+[\t ]*(?:#[^\n]*)?/
94
+ workflow_ref_pattern =
95
+ %r{(uses:\s+shakacode/control-plane-flow/\.github/workflows/[^@\s]+@)[^\s#]+[\t ]*(?:#[^\n]*)?}
72
96
  updated = text
73
- .gsub(%r{(uses:\s+shakacode/control-plane-flow/\.github/workflows/[^@\s]+@)[^\s]+}, "\\1#{ref}")
74
- .gsub(production_setup_ref_pattern, "\\1#{ref}")
75
- .gsub(CPFLOW_CHECKOUT_REF_PATTERN, "\\1#{ref}")
97
+ .gsub(workflow_ref_pattern) { "#{Regexp.last_match(1)}#{annotated_ref}" }
98
+ .gsub(production_setup_ref_pattern) { "#{Regexp.last_match(1)}#{annotated_ref}" }
99
+ .gsub(CPFLOW_CHECKOUT_REF_PATTERN) { "#{Regexp.last_match(1)}#{annotated_ref}" }
76
100
 
77
101
  next if updated == text
78
102
 
@@ -142,60 +142,65 @@ Dir[".github/workflows/cpflow-*.yml"].sort.each do |path|
142
142
  end
143
143
 
144
144
  promote_path = ".github/workflows/cpflow-promote-staging-to-production.yml"
145
- promote_doc = YAML.load_file(promote_path, aliases: true)
146
- promote_job = promote_doc.fetch("jobs", {})["promote-to-production"]
145
+ if File.exist?(promote_path)
146
+ promote_doc = YAML.load_file(promote_path, aliases: true)
147
+ promote_job = promote_doc.fetch("jobs", {})["promote-to-production"]
147
148
 
148
- unless promote_job
149
- abort "#{promote_path}:promote-to-production job is missing"
150
- end
149
+ unless promote_job
150
+ abort "#{promote_path}:promote-to-production job is missing"
151
+ end
151
152
 
152
- if promote_job.key?("uses")
153
- abort "#{promote_path}:promote-to-production must run as a normal caller-repo job, not a reusable workflow, so GitHub can expose production environment secrets"
154
- end
153
+ if promote_job.key?("uses")
154
+ abort "#{promote_path}:promote-to-production must run as a normal caller-repo job, not a reusable workflow, so GitHub can expose production environment secrets"
155
+ end
155
156
 
156
- unless promote_job["environment"].to_s == "production"
157
- abort "#{promote_path}:promote-to-production must declare environment: production"
158
- end
157
+ # Older generated workflows use the string form; current ones add a deployment URL.
158
+ promote_environment = promote_job["environment"]
159
+ promote_environment = promote_environment["name"] if promote_environment.is_a?(Hash)
160
+ unless promote_environment.to_s == "production"
161
+ abort "#{promote_path}:promote-to-production must declare environment: production"
162
+ end
159
163
 
160
- checkout_step = Array(promote_job["steps"]).find { |step| step["name"] == "Checkout control-plane-flow actions" }
164
+ checkout_step = Array(promote_job["steps"]).find { |step| step["name"] == "Checkout control-plane-flow actions" }
161
165
 
162
- unless checkout_step
163
- abort "#{promote_path}:promote-to-production must include a Checkout control-plane-flow actions step"
164
- end
166
+ unless checkout_step
167
+ abort "#{promote_path}:promote-to-production must include a Checkout control-plane-flow actions step"
168
+ end
165
169
 
166
- unless checkout_step["uses"] == EXPECTED_CPFLOW_CHECKOUT_ACTION
167
- abort "#{promote_path}:promote-to-production must use #{EXPECTED_CPFLOW_CHECKOUT_ACTION} for the Checkout control-plane-flow actions step"
168
- end
170
+ unless checkout_step["uses"] == EXPECTED_CPFLOW_CHECKOUT_ACTION
171
+ abort "#{promote_path}:promote-to-production must use #{EXPECTED_CPFLOW_CHECKOUT_ACTION} for the Checkout control-plane-flow actions step"
172
+ end
169
173
 
170
- checkout_with = checkout_step.fetch("with", {})
171
- checkout_repository = checkout_with["repository"]
172
- checkout_ref = checkout_with["ref"]
174
+ checkout_with = checkout_step.fetch("with", {})
175
+ checkout_repository = checkout_with["repository"]
176
+ checkout_ref = checkout_with["ref"]
173
177
 
174
- unless checkout_repository == EXPECTED_CPFLOW_CHECKOUT_REPOSITORY
175
- abort "#{promote_path}:promote-to-production must check out #{EXPECTED_CPFLOW_CHECKOUT_REPOSITORY}"
176
- end
178
+ unless checkout_repository == EXPECTED_CPFLOW_CHECKOUT_REPOSITORY
179
+ abort "#{promote_path}:promote-to-production must check out #{EXPECTED_CPFLOW_CHECKOUT_REPOSITORY}"
180
+ end
177
181
 
178
- if checkout_ref.to_s.strip.empty?
179
- abort "#{promote_path}:promote-to-production must pin the Checkout control-plane-flow actions step"
180
- end
182
+ if checkout_ref.to_s.strip.empty?
183
+ abort "#{promote_path}:promote-to-production must pin the Checkout control-plane-flow actions step"
184
+ end
181
185
 
182
- refs[checkout_ref] << "#{promote_path}:promote-to-production"
186
+ refs[checkout_ref] << "#{promote_path}:promote-to-production"
183
187
 
184
- setup_step = Array(promote_job["steps"]).find { |step| step["name"] == "Setup production environment" }
188
+ setup_step = Array(promote_job["steps"]).find { |step| step["name"] == "Setup production environment" }
185
189
 
186
- unless setup_step
187
- abort "#{promote_path}:promote-to-production must include a Setup production environment step"
188
- end
190
+ unless setup_step
191
+ abort "#{promote_path}:promote-to-production must include a Setup production environment step"
192
+ end
189
193
 
190
- setup_ref = setup_step.fetch("with", {})["control_plane_flow_ref"]
191
- setup_match = setup_ref.to_s.match(PROMOTE_WORKFLOW)
194
+ setup_ref = setup_step.fetch("with", {})["control_plane_flow_ref"]
195
+ setup_match = setup_ref.to_s.match(PROMOTE_WORKFLOW)
192
196
 
193
- unless setup_match
194
- abort "#{promote_path}:promote-to-production must pass a pinned production control_plane_flow_ref to setup, " \
195
- "for example #{EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT}"
196
- end
197
+ unless setup_match
198
+ abort "#{promote_path}:promote-to-production must pass a pinned production control_plane_flow_ref to setup, " \
199
+ "for example #{EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT}"
200
+ end
197
201
 
198
- refs[setup_match[1]] << "#{promote_path}:promote-to-production setup"
202
+ refs[setup_match[1]] << "#{promote_path}:promote-to-production setup"
203
+ end
199
204
 
200
205
  if refs.empty?
201
206
  puts "no upstream cpflow reusable workflow refs found"
@@ -210,7 +215,21 @@ end
210
215
  RUBY
211
216
 
212
217
  echo "==> actionlint"
213
- actionlint \
214
- -ignore "SC2129" \
215
- -ignore 'property "workflow_(ref|sha|repository|file_path)" is not defined in object type' \
216
- .github/workflows/cpflow-*.yml
218
+ shopt -s nullglob
219
+ cpflow_workflows=(.github/workflows/cpflow-*.yml)
220
+ if [[ ${#cpflow_workflows[@]} -gt 0 ]]; then
221
+ actionlint \
222
+ -ignore "SC2129" \
223
+ -ignore 'property "workflow_(ref|sha|repository|file_path)" is not defined in object type' \
224
+ "${cpflow_workflows[@]}"
225
+ fi
226
+
227
+ custom_validator="bin/test-cpflow-github-flow-custom"
228
+ if [[ -e "$custom_validator" || -L "$custom_validator" ]]; then
229
+ if [[ ! -f "$custom_validator" || ! -x "$custom_validator" ]]; then
230
+ echo "Custom validator must be an executable file: $custom_validator" >&2
231
+ exit 1
232
+ fi
233
+ echo "==> downstream cpflow GitHub flow checks"
234
+ "$ROOT/$custom_validator" "${cpflow_cmd[@]}"
235
+ fi
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cpflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.rc.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
@@ -82,6 +82,9 @@ files:
82
82
  - ".agents/bin/setup"
83
83
  - ".agents/bin/test"
84
84
  - ".agents/bin/validate"
85
+ - ".agents/legacy-workflow-policy.yml"
86
+ - ".agents/shaka.md"
87
+ - ".agents/trusted-actions.yml"
85
88
  - ".agents/trusted-github-actors.yml"
86
89
  - ".agents/workflows/address-review.md"
87
90
  - ".agents/workflows/ai-rollout-e2e-test.md"
@@ -92,6 +95,8 @@ files:
92
95
  - ".github/actions/cpflow-delete-control-plane-app/action.yml"
93
96
  - ".github/actions/cpflow-delete-control-plane-app/delete-app.sh"
94
97
  - ".github/actions/cpflow-detect-release-phase/action.yml"
98
+ - ".github/actions/cpflow-preview-promotion/action.yml"
99
+ - ".github/actions/cpflow-preview-promotion/preview-promotion.sh"
95
100
  - ".github/actions/cpflow-resolve-review-config/action.yml"
96
101
  - ".github/actions/cpflow-setup-environment/action.yml"
97
102
  - ".github/actions/cpflow-validate-config/action.yml"
@@ -154,6 +159,7 @@ files:
154
159
  - docs/dns.md
155
160
  - docs/grafana-opentelemetry.md
156
161
  - docs/migrating-heroku-to-control-plane.md
162
+ - docs/postgres-review-apps.md
157
163
  - docs/postgres.md
158
164
  - docs/rds-private-networking.md
159
165
  - docs/redis.md
@@ -225,6 +231,7 @@ files:
225
231
  - lib/core/controlplane.rb
226
232
  - lib/core/controlplane_api.rb
227
233
  - lib/core/controlplane_api_direct.rb
234
+ - lib/core/disposable_postgres_helpers.rb
228
235
  - lib/core/doctor_service.rb
229
236
  - lib/core/github_flow_readiness/checks.rb
230
237
  - lib/core/github_flow_readiness_service.rb
@@ -254,6 +261,7 @@ files:
254
261
  - lib/cpflow.rb
255
262
  - lib/cpflow/version.rb
256
263
  - lib/deprecated_commands.json
264
+ - lib/generator_templates/.dockerignore
257
265
  - lib/generator_templates/Dockerfile
258
266
  - lib/generator_templates/controlplane.yml
259
267
  - lib/generator_templates/entrypoint.sh
@@ -309,7 +317,7 @@ licenses:
309
317
  metadata:
310
318
  rubygems_mfa_required: 'true'
311
319
  post_install_message: |
312
- cpflow 6.0.0.rc.0 installed.
320
+ cpflow 6.0.0 installed.
313
321
  Generated GitHub Actions users: run `cpflow update-github-actions` after upgrading.
314
322
  Docs: https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md#updating-generated-github-actions-after-gem-updates
315
323
  rdoc_options: []
@@ -326,7 +334,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
326
334
  - !ruby/object:Gem::Version
327
335
  version: '0'
328
336
  requirements: []
329
- rubygems_version: 4.0.18
337
+ rubygems_version: 4.0.8
330
338
  specification_version: 4
331
339
  summary: Control Plane Flow
332
340
  test_files: []