From f546d7971fc4f7ea214f7a18a63e4f27f16e952c Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 2 Jun 2026 17:02:52 -1000 Subject: [PATCH 1/3] Harden production promotion workflow --- .../docs/testing-cpflow-github-actions.md | 6 +- .controlplane/readme.md | 25 ++- .controlplane/shakacode-team.md | 25 ++- .github/cpflow-help.md | 42 ++-- .../cpflow-cleanup-stale-review-apps.yml | 2 +- .../workflows/cpflow-delete-review-app.yml | 2 +- .../workflows/cpflow-deploy-review-app.yml | 2 +- .github/workflows/cpflow-deploy-staging.yml | 2 +- .github/workflows/cpflow-help-command.yml | 2 +- .../cpflow-promote-staging-to-production.yml | 185 ++++++++++++++---- .github/workflows/cpflow-review-app-help.yml | 2 +- Gemfile | 2 +- Gemfile.lock | 6 +- 13 files changed, 211 insertions(+), 92 deletions(-) diff --git a/.controlplane/docs/testing-cpflow-github-actions.md b/.controlplane/docs/testing-cpflow-github-actions.md index dd5ec70a..142936a4 100644 --- a/.controlplane/docs/testing-cpflow-github-actions.md +++ b/.controlplane/docs/testing-cpflow-github-actions.md @@ -29,8 +29,10 @@ bin/pin-cpflow-github-ref <40-character-control-plane-flow-commit-sha> bin/conductor-exec bin/test-cpflow-github-flow ruby /path/to/control-plane-flow/bin/cpflow ``` -Leave `CPFLOW_VERSION` unset while testing a commit SHA. After the upstream gem -and tag ship, repin wrappers to the release tag, such as `v5.0.4`. +Leave `CPFLOW_VERSION` unset while testing a commit SHA. After the upstream PR +ships in a release tag, repin wrappers to that tag. Use `v5.1.0` only for +changes already included in that tag; keep this promotion-hardening canary on an +immutable commit SHA until the upstream hardening PR is released. ## Review App Canary diff --git a/.controlplane/readme.md b/.controlplane/readme.md index 311f4f34..45488fd3 100644 --- a/.controlplane/readme.md +++ b/.controlplane/readme.md @@ -122,7 +122,16 @@ The matching Control Plane resources are: | Production app secret dictionary | `react-webpack-rails-tutorial-production-secrets` | Bootstrap production the same way before the first promotion, using the -production org and production-only secret values. +production org and production-only secret values. After bootstrap or any +template change, re-apply the persistent production templates so the `rails` +and `daily-task` workloads keep the same secret-backed env names as staging: + +```sh +cpflow apply-template app postgres redis daily-task rails \ + -a react-webpack-rails-tutorial-production \ + --org shakacode-open-source-examples-production \ + --yes --add-app-identity +``` All review, staging, and production secret dictionaries need these app runtime secrets: @@ -529,8 +538,10 @@ If staging moves off `master`, update both the `STAGING_APP_BRANCH` repository variable and the `branches:` filter in `.github/workflows/cpflow-deploy-staging.yml`; GitHub does not allow repository variables in trigger branch filters. The production promotion workflow checks that production has all environment -variable names present in staging; it does not compare secret values, workload -environment variables, or Control Plane secret references. +variable names present in staging at both the GVC level and each configured app +workload's container level. It does not compare secret values. The health check +waits for Control Plane to report both `status.ready` and `status.readyLatest` +before probing the public endpoint. The GitHub settings and Control Plane resources must match the app names in `.controlplane/controlplane.yml`. For the standard review-app path, leave @@ -564,9 +575,11 @@ Keep the reusable-workflow mechanics in the upstream For this repo, the update loop is: 1. Generate from the desired `cpflow` release with `--staging-branch master`. -2. Keep generated refs on a release tag such as `v5.0.4`. Use a full upstream - commit SHA only for short-lived downstream testing of an unreleased upstream - PR, and leave `CPFLOW_VERSION` unset in that case. +2. Keep generated refs on a release tag once the upstream hardening changes ship. + This branch temporarily pins refs to + `9ef104c246670d6c1ea4132dfd22be68ef930a70` to test upstream promotion + hardening before the next release tag. Leave `CPFLOW_VERSION` unset while + testing a commit SHA. 3. Keep app names and GitHub settings aligned with `.controlplane/controlplane.yml`. 4. Validate locally: diff --git a/.controlplane/shakacode-team.md b/.controlplane/shakacode-team.md index 38bf3e06..358f383e 100644 --- a/.controlplane/shakacode-team.md +++ b/.controlplane/shakacode-team.md @@ -102,14 +102,27 @@ cpflow setup-app -a react-webpack-rails-tutorial-production --org shakacode-open Use `setup-app` for first-time bootstrap because it creates the app secret policy and identity binding. Use `cpflow apply-template` for later template -updates to existing persistent apps. +updates to existing persistent apps. Production promotion compares both GVC env +names and app workload container env names against staging before copying the +image, so keep production `rails` and `daily-task` env references in sync with +the templates: + +```sh +cpflow apply-template app postgres redis daily-task rails \ + -a react-webpack-rails-tutorial-production \ + --org shakacode-open-source-examples-production \ + --yes --add-app-identity +``` Advanced optional settings are documented upstream in the [`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md). -Current workflow wrappers are pinned to the upstream `control-plane-flow` -release tag `v5.0.4`. Keep release tags as the steady-state configuration; use -a full commit SHA only for short-lived upstream PR testing. +Current workflow wrappers are temporarily pinned to upstream +`control-plane-flow` commit `9ef104c246670d6c1ea4132dfd22be68ef930a70` to test +promotion hardening before it ships in a release tag. Keep release tags as the +steady-state configuration once the upstream PR is released; use a full commit +SHA only for short-lived upstream PR testing and leave `CPFLOW_VERSION` unset in +that case. If staging moves off `master`, update both `STAGING_APP_BRANCH` and the branch filter in `.github/workflows/cpflow-deploy-staging.yml`. @@ -119,8 +132,8 @@ filter in `.github/workflows/cpflow-deploy-staging.yml`. When the upstream `control-plane-flow` repo changes the generated GitHub Actions flow, regenerate from the target `cpflow` version with `--staging-branch master`, review the diff, and validate with `bin/test-cpflow-github-flow` plus the normal -CI checks. Stable automation should use release tags such as `v5.0.4`, not -`main` or a feature branch. +CI checks. Stable automation should use a release tag that includes the upstream +hardening changes, not `main` or a feature branch. See [readme.md](readme.md) and [Testing cpflow GitHub Actions Changes](docs/testing-cpflow-github-actions.md) diff --git a/.github/cpflow-help.md b/.github/cpflow-help.md index dfbbc0c5..7112118c 100644 --- a/.github/cpflow-help.md +++ b/.github/cpflow-help.md @@ -2,7 +2,7 @@ These commands are generated by [cpflow](https://github.com/shakacode/control-plane-flow). For full setup, version-pinning, and troubleshooting details, see the upstream -[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/v5.0.4/docs/ci-automation.md). +[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/9ef104c246670d6c1ea4132dfd22be68ef930a70/docs/ci-automation.md). ## Pull Request Commands @@ -23,23 +23,11 @@ For the normal generated review-app path, GitHub needs one repository secret: | --- | --- | --- | | `CPLN_TOKEN_STAGING` | Repository secret | Control Plane service-account token for the staging/review org. | -For public repositories, use a staging/review token that cannot access -production Control Plane resources. Generated review-app deploys skip fork PR -heads because Docker builds use repository secrets. If a forked change needs a -review app, first move the reviewed change to a trusted branch in this -repository. - No repository variables are required for the standard review-app path when `.controlplane/controlplane.yml` has exactly one review app entry with `match_if_app_name_starts_with: true`. cpflow infers the review-app prefix and staging org from that config. -Review apps run pull request code. Any value mounted through -`cpln://secret/...` can be read by that code after the workload starts, so keep -review-app secret dictionaries limited to disposable databases, review-only -renderer credentials, and license values that are acceptable for review-app -exposure. - Optional overrides exist for forks, clones, and unusual apps: | Name | Notes | @@ -89,7 +77,7 @@ normal environment-gated job cannot tell which secret scope supplied a nonempty value, so a broader secret with the same name can mask a missing environment secret. -If promotion fails with +If the promotion workflow fails with `CPLN_TOKEN_PRODUCTION is not set. Add it as a secret on the 'production' GitHub Environment.`, the token is missing from the environment scope or the workflow job is no longer declaring `environment: production`. Create or verify the environment secret @@ -98,10 +86,11 @@ You need permission to manage repository environments and secrets to run these commands. ```sh -gh secret set CPLN_TOKEN_PRODUCTION --repo shakacode/react-webpack-rails-tutorial --env production -gh secret list --repo shakacode/react-webpack-rails-tutorial --env production -gh secret list --repo shakacode/react-webpack-rails-tutorial -gh secret list --org shakacode | grep '^CPLN_TOKEN_PRODUCTION[[:space:]]' || true +gh secret set CPLN_TOKEN_PRODUCTION --repo OWNER/REPO --env production +# Paste the token value when prompted. +gh secret list --repo OWNER/REPO --env production +gh secret list --repo OWNER/REPO +gh secret list --org OWNER | grep '^CPLN_TOKEN_PRODUCTION[[:space:]]' || true ``` Before the first promotion, bootstrap the production app the same way in the @@ -109,16 +98,19 @@ production org, using production-only secrets and values. ## Version Locking -Generated wrappers pin Control Plane Flow with a release tag, for example -`v5.0.4`. Reusable review-app, staging, cleanup, and helper workflows pin the -tag in their `uses:` ref. Production promotion pins the same tag in the -`Checkout control-plane-flow actions` step so the caller-owned job can keep +Generated wrappers normally pin Control Plane Flow with a release tag, for +example `v5.1.0`. This branch temporarily pins the wrappers to upstream commit +`9ef104c246670d6c1ea4132dfd22be68ef930a70` while testing unreleased production +promotion hardening. Reusable review-app, staging, cleanup, and helper workflows +pin that ref in their `uses:` entry. Production promotion pins the same ref in +the `Checkout control-plane-flow actions` step so the caller-owned job can keep `environment: production` and receive production environment secrets directly. Leave `CPFLOW_VERSION` unset so the workflow builds cpflow from the same checked-out upstream source. If you set `CPFLOW_VERSION`, it must match the -release tag, for example `CPFLOW_VERSION=5.0.4` with a wrapper pinned to -`uses: ...@v5.0.4`. +release tag your wrappers are pinned to: a `CPFLOW_VERSION=5.1.x` runtime +override goes with a wrapper pinned to `uses: ...@v5.1.x` (substitute the +release you pinned above). After updating the `cpflow` gem in this repo, update the generated wrappers in the same PR: @@ -151,7 +143,7 @@ Most apps do not need these: | Name | Notes | | --- | --- | | `DOCKER_BUILD_EXTRA_ARGS` | Newline-delimited extra Docker build tokens. | -| `DOCKER_BUILD_SSH_KEY` | Read-only, revocable deploy key for Docker builds that fetch private dependencies. Do not use a personal SSH key. | +| `DOCKER_BUILD_SSH_KEY` | Private SSH key for Docker builds that fetch private dependencies. | | `DOCKER_BUILD_SSH_KNOWN_HOSTS` | SSH known_hosts entries when SSH build hosts are not GitHub.com. | | `REVIEW_APP_DEPLOYING_ICON_URL` | Cosmetic custom image URL for the animated deploying icon. Set to `none` to use the text fallback icon. | | `STAGING_APP_BRANCH` | Custom staging branch. The branch must also appear in `cpflow-deploy-staging.yml`'s push filter. | diff --git a/.github/workflows/cpflow-cleanup-stale-review-apps.yml b/.github/workflows/cpflow-cleanup-stale-review-apps.yml index d3fb09d8..371ebcb2 100644 --- a/.github/workflows/cpflow-cleanup-stale-review-apps.yml +++ b/.github/workflows/cpflow-cleanup-stale-review-apps.yml @@ -12,6 +12,6 @@ jobs: cleanup: # Cleanup targets the current inferred review-app prefix. If you changed # naming conventions, manually delete review apps under the old prefix. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-delete-review-app.yml b/.github/workflows/cpflow-delete-review-app.yml index cda14843..c45f3431 100644 --- a/.github/workflows/cpflow-delete-review-app.yml +++ b/.github/workflows/cpflow-delete-review-app.yml @@ -31,6 +31,6 @@ jobs: github.event_name == 'workflow_dispatch' # This `if:` mirrors the upstream job guard to avoid a billable workflow_call # when the event does not match. Keep both conditions in sync. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-deploy-review-app.yml b/.github/workflows/cpflow-deploy-review-app.yml index c228e472..92749113 100644 --- a/.github/workflows/cpflow-deploy-review-app.yml +++ b/.github/workflows/cpflow-deploy-review-app.yml @@ -30,7 +30,7 @@ jobs: github.event.issue.pull_request && contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }} diff --git a/.github/workflows/cpflow-deploy-staging.yml b/.github/workflows/cpflow-deploy-staging.yml index be96cdf9..000a5d50 100644 --- a/.github/workflows/cpflow-deploy-staging.yml +++ b/.github/workflows/cpflow-deploy-staging.yml @@ -16,7 +16,7 @@ permissions: jobs: deploy-staging: - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 with: staging_app_branch_default: "master" secrets: diff --git a/.github/workflows/cpflow-help-command.yml b/.github/workflows/cpflow-help-command.yml index b12697cd..429cfcb7 100644 --- a/.github/workflows/cpflow-help-command.yml +++ b/.github/workflows/cpflow-help-command.yml @@ -23,4 +23,4 @@ jobs: contains(fromJson('["+review-app-help","+review-app-help\n","+review-app-help\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || github.event_name == 'workflow_dispatch' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 diff --git a/.github/workflows/cpflow-promote-staging-to-production.yml b/.github/workflows/cpflow-promote-staging-to-production.yml index 7b6f0e73..20d8a714 100644 --- a/.github/workflows/cpflow-promote-staging-to-production.yml +++ b/.github/workflows/cpflow-promote-staging-to-production.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: repository: shakacode/control-plane-flow - ref: v5.0.4 + ref: 9ef104c246670d6c1ea4132dfd22be68ef930a70 path: .cpflow persist-credentials: false @@ -140,7 +140,7 @@ jobs: if ! [[ "${value}" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then local display_value display_value="$(printf '%q' "${value}")" - echo "::error::${label} (${display_value}) must be a valid Control Plane org name; use lowercase alphanumeric characters and hyphens only, with no leading or trailing hyphen." + echo "::error::${label} (${display_value}) must be a valid Control Plane org name; use lowercase alphanumeric characters and hyphens only, with no leading or trailing hyphen." >&2 exit 1 fi } @@ -179,7 +179,7 @@ jobs: cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }} cpflow_version: ${{ vars.CPFLOW_VERSION }} # The setup action validates CPFLOW_VERSION against this full workflow ref. - control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@v5.0.4 + control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 # Runs after Setup production environment so the pinned Ruby (>= 3.1) is on PATH. # YAML.load_file(..., aliases: true) is not supported on Ruby 3.0 (system Ruby on ubuntu-22.04). @@ -249,34 +249,89 @@ jobs: PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} CPLN_ORG_STAGING: ${{ steps.cpln-orgs.outputs.staging }} CPLN_ORG_PRODUCTION: ${{ steps.cpln-orgs.outputs.production }} + WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }} shell: bash run: | set -euo pipefail - staging_vars="$(CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln gvc get "${STAGING_APP_NAME}" --org "${CPLN_ORG_STAGING}" -o json | jq -r '.spec.env // [] | .[].name' | sort)" - production_vars="$(CPLN_TOKEN="${CPLN_TOKEN_PRODUCTION}" cpln gvc get "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" -o json | jq -r '.spec.env // [] | .[].name' | sort)" + list_gvc_env_names() { + local token="$1" + local org="$2" + local app="$3" - if [[ -z "${staging_vars}" ]]; then - echo "Staging GVC exposes no environment variables; skipping parity check." - exit 0 - fi + CPLN_TOKEN="${token}" cpln gvc get "${app}" --org "${org}" -o json | + jq -r '.spec.env // [] | .[] | .name // empty' | + sort -u + } - # Treat staging as the promotion source of truth: fail when a variable - # present in staging is missing in production. Production-only variables - # are allowed, but surface them so teams can spot drift. - missing_vars="$(comm -23 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))" - production_only_vars="$(comm -13 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))" + list_workload_env_names() { + local token="$1" + local org="$2" + local app="$3" + local workload="$4" - if [[ -n "${production_only_vars}" ]]; then - echo "::warning::Production has environment variables that are not present in staging:" - echo "${production_only_vars}" - fi + CPLN_TOKEN="${token}" cpln workload get "${workload}" --gvc "${app}" --org "${org}" -o json | + jq -r '.spec.containers // [] | .[] | (.env // [])[]? | .name // empty' | + sort -u + } - if [[ -n "${missing_vars}" ]]; then - echo "::error::Production is missing environment variables that exist in staging" - echo "${missing_vars}" - exit 1 - fi + check_required_vars() { + local staging_scope="$1" + local production_scope="$2" + local missing_message="$3" + local staging_vars="$4" + local production_vars="$5" + local missing_vars + local production_only_vars + + if [[ -z "${staging_vars}" ]]; then + echo "Staging ${staging_scope} exposes no environment variables; skipping parity check." + return + fi + + # Treat staging as the promotion source of truth: fail when a variable + # present in staging is missing in production. Production-only variables + # are allowed, but surface them so teams can spot drift. + missing_vars="$(comm -23 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))" + production_only_vars="$(comm -13 <(printf '%s\n' "${staging_vars}") <(printf '%s\n' "${production_vars}"))" + + if [[ -n "${production_only_vars}" ]]; then + echo "::warning::Production ${production_scope} has environment variables that are not present in staging:" + echo "${production_only_vars}" + fi + + if [[ -n "${missing_vars}" ]]; then + echo "::error::${missing_message}" + echo "${missing_vars}" + env_check_failed=1 + fi + } + + env_check_failed=0 + + staging_vars="$(list_gvc_env_names "${CPLN_TOKEN_STAGING}" "${CPLN_ORG_STAGING}" "${STAGING_APP_NAME}")" + production_vars="$(list_gvc_env_names "${CPLN_TOKEN_PRODUCTION}" "${CPLN_ORG_PRODUCTION}" "${PRODUCTION_APP_NAME}")" + check_required_vars \ + "GVC '${STAGING_APP_NAME}'" \ + "GVC '${PRODUCTION_APP_NAME}'" \ + "Production GVC '${PRODUCTION_APP_NAME}' is missing environment variables that exist in staging" \ + "${staging_vars}" \ + "${production_vars}" + + while IFS= read -r workload_name; do + [[ -n "${workload_name}" ]] || continue + + staging_workload_vars="$(list_workload_env_names "${CPLN_TOKEN_STAGING}" "${CPLN_ORG_STAGING}" "${STAGING_APP_NAME}" "${workload_name}")" + production_workload_vars="$(list_workload_env_names "${CPLN_TOKEN_PRODUCTION}" "${CPLN_ORG_PRODUCTION}" "${PRODUCTION_APP_NAME}" "${workload_name}")" + check_required_vars \ + "workload '${workload_name}'" \ + "workload '${workload_name}'" \ + "Production workload '${workload_name}' is missing environment variables that exist in staging" \ + "${staging_workload_vars}" \ + "${production_workload_vars}" + done < <(tr ',' '\n' <<< "${WORKLOAD_NAMES}") + + exit "${env_check_failed}" - name: Capture current production image id: capture-current @@ -382,10 +437,12 @@ jobs: echo "image=${staging_image}" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 + - name: Copy image from staging id: copy-image env: - # Pass the upstream token via env rather than `-t` so it doesn't appear in /proc//cmdline. CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} CPLN_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }} PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} @@ -410,27 +467,53 @@ jobs: copy_image_attempts=$((copy_image_retries + 1)) copy_image_retry_interval=$((10#${COPY_IMAGE_RETRY_INTERVAL})) - if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${STAGING_IMAGE}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then - echo "::error::Staging image '${STAGING_IMAGE}' was not found in org '${CPLN_ORG_STAGING}'; aborting promotion." + if [[ "${STAGING_IMAGE}" == *@* ]]; then + staging_image="${STAGING_IMAGE}" + else + staging_image="${STAGING_IMAGE%%@*}" + fi + if [[ -z "${staging_image}" ]]; then + echo "::error::Staging image '${STAGING_IMAGE}' did not contain a usable image reference." exit 1 fi - staging_commit="${STAGING_IMAGE##*_}" - if [[ "${staging_commit}" == "${STAGING_IMAGE}" || -z "${staging_commit}" ]]; then - echo "::error::Staging image '${STAGING_IMAGE}' does not include the expected '_' suffix." + if ! CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${staging_image}" --org "${CPLN_ORG_STAGING}" -o json >/dev/null; then + echo "::error::Staging image '${STAGING_IMAGE}' was not found in org '${CPLN_ORG_STAGING}'; aborting promotion." exit 1 fi + staging_tag="" + if [[ "${staging_image}" == *@* ]]; then + staging_tag="${staging_image##*@}" + elif [[ "${staging_image}" == *:* ]]; then + staging_tag="${staging_image##*:}" + fi + staging_commit="" + if [[ "${staging_tag}" == *_* ]]; then + staging_commit="${staging_tag##*_}" + fi + + # The workflow-level concurrency group serializes this sequence so two + # production promotions cannot derive and publish the same next tag. latest_number="$( - cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" -o json | + cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" --max 0 -o json | jq -r --arg prefix "${PRODUCTION_APP_NAME}:" \ '[.items[].name | select(startswith($prefix)) | (try capture("^[^:]+:(?[0-9]+)") catch empty) | .number | tonumber] | max // 0' )" - production_image="${PRODUCTION_APP_NAME}:$((latest_number + 1))_${staging_commit}" + if ! [[ "${latest_number}" =~ ^[0-9]+$ ]]; then + echo "::error::Could not determine the next production image number for app '${PRODUCTION_APP_NAME}' in org '${CPLN_ORG_PRODUCTION}'." + exit 1 + fi + + production_image="${PRODUCTION_APP_NAME}:$((latest_number + 1))" + if [[ -n "${staging_commit}" ]]; then + production_image="${production_image}_${staging_commit}" + fi + staging_registry="${CPLN_ORG_STAGING}.registry.cpln.io" production_registry="${CPLN_ORG_PRODUCTION}.registry.cpln.io" - source_image_ref="${CPLN_ORG_STAGING}.registry.cpln.io/${STAGING_IMAGE}" - production_image_ref="${CPLN_ORG_PRODUCTION}.registry.cpln.io/${production_image}" + source_image_ref="${staging_registry}/${STAGING_IMAGE}" + production_image_ref="${production_registry}/${production_image}" docker_config_dir="$(mktemp -d)" cleanup_copy_credentials() { @@ -440,16 +523,27 @@ jobs: export DOCKER_CONFIG="${docker_config_dir}" + if ! printf '%s' "${CPLN_TOKEN_STAGING}" | + docker login "${staging_registry}" -u '' --password-stdin >/dev/null; then + echo "::error::Failed to authenticate to staging registry '${staging_registry}'." + exit 1 + fi + + if ! printf '%s' "${CPLN_TOKEN_PRODUCTION}" | + docker login "${production_registry}" -u '' --password-stdin >/dev/null; then + echo "::error::Failed to authenticate to production registry '${production_registry}'." + exit 1 + fi + + if docker buildx imagetools inspect "${production_image_ref}" >/dev/null 2>&1; then + echo "::error::Production image '${production_image}' already exists in org '${CPLN_ORG_PRODUCTION}'; aborting to avoid overwriting it." + exit 1 + fi + copy_status=1 for attempt in $(seq 1 "${copy_image_attempts}"); do - if printf '%s' "${CPLN_TOKEN_STAGING}" | - docker login "${staging_registry}" -u '' --password-stdin >/dev/null && - printf '%s' "${CPLN_TOKEN_PRODUCTION}" | - docker login "${production_registry}" -u '' --password-stdin >/dev/null && - docker manifest inspect "${source_image_ref}" >/dev/null && - docker pull "${source_image_ref}" && - docker tag "${source_image_ref}" "${production_image_ref}" && - docker push "${production_image_ref}"; then + if docker buildx imagetools inspect "${source_image_ref}" >/dev/null && + docker buildx imagetools create --prefer-index=false --tag "${production_image_ref}" "${source_image_ref}"; then copy_status=0 break else @@ -484,6 +578,9 @@ jobs: if [[ -n "${RELEASE_PHASE_FLAG}" ]]; then deploy_args+=("${RELEASE_PHASE_FLAG}") fi + # `cpflow deploy-image` deploys the latest image for the app. The + # workflow-level concurrency group keeps production promotion copy and + # deploy steps coupled across workflow runs. deploy_args+=(--org "${CPLN_ORG_PRODUCTION}" --verbose) cpflow deploy-image "${deploy_args[@]}" @@ -592,8 +689,10 @@ jobs: set -euo pipefail ready=false for attempt in $(seq 1 "${ROLLBACK_READINESS_RETRIES}"); do - deployment_ready="$(cpln workload get "${workload_name}" --gvc "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" -o json | jq -r '.status.ready // false')" - if [[ "${deployment_ready}" == "true" ]]; then + workload_status="$(cpln workload get "${workload_name}" --gvc "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" -o json)" + deployment_ready="$(echo "${workload_status}" | jq -r '.status.ready // false')" + latest_ready="$(echo "${workload_status}" | jq -r '.status.readyLatest // false')" + if [[ "${deployment_ready}" == "true" && "${latest_ready}" == "true" ]]; then ready=true break fi diff --git a/.github/workflows/cpflow-review-app-help.yml b/.github/workflows/cpflow-review-app-help.yml index c8b24e15..4e5ea326 100644 --- a/.github/workflows/cpflow-review-app-help.yml +++ b/.github/workflows/cpflow-review-app-help.yml @@ -18,4 +18,4 @@ jobs: # to PR-open help. Remove it, or uncomment and adapt this guard, if forks or # clones should stay quiet until Control Plane is configured: # if: vars.REVIEW_APP_PREFIX != '' || vars.CPLN_ORG_STAGING != '' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@v5.0.4 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 diff --git a/Gemfile b/Gemfile index e5df918f..bbf26705 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "3.4.6" -gem "cpflow", "5.0.4", require: false +gem "cpflow", "5.1.0", require: false gem "react_on_rails_pro", "16.7.0.rc.3" gem "shakapacker", "10.1.0" diff --git a/Gemfile.lock b/Gemfile.lock index 7b7b3561..469afbaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,7 +143,7 @@ GEM term-ansicolor (~> 1.6) thor (>= 0.20.3, < 2.0) tins (~> 1.16) - cpflow (5.0.4) + cpflow (5.1.0) dotenv (~> 3.1) jwt (~> 3.1) psych (~> 5.2) @@ -282,7 +282,7 @@ GEM pry-stack_explorer (0.6.1) binding_of_caller (~> 1.0) pry (~> 0.13) - psych (5.3.1) + psych (5.4.0) date stringio public_suffix (7.0.5) @@ -529,7 +529,7 @@ DEPENDENCIES capybara-screenshot coffee-rails coveralls_reborn (~> 0.25.0) - cpflow (= 5.0.4) + cpflow (= 5.1.0) database_cleaner debug (>= 1.0.0) factory_bot_rails From 13ef0716bd07d5008a6b54fbcab0b65903cc1c32 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 2 Jun 2026 18:45:26 -1000 Subject: [PATCH 2/3] Regenerate promotion wrappers after review follow-ups --- .controlplane/readme.md | 2 +- .controlplane/shakacode-team.md | 2 +- .github/cpflow-help.md | 27 ++++++++++++++----- .../cpflow-cleanup-stale-review-apps.yml | 2 +- .../workflows/cpflow-delete-review-app.yml | 2 +- .../workflows/cpflow-deploy-review-app.yml | 2 +- .github/workflows/cpflow-deploy-staging.yml | 2 +- .github/workflows/cpflow-help-command.yml | 2 +- .../cpflow-promote-staging-to-production.yml | 15 +++++------ .github/workflows/cpflow-review-app-help.yml | 2 +- 10 files changed, 35 insertions(+), 23 deletions(-) diff --git a/.controlplane/readme.md b/.controlplane/readme.md index 45488fd3..c5c23bfc 100644 --- a/.controlplane/readme.md +++ b/.controlplane/readme.md @@ -577,7 +577,7 @@ For this repo, the update loop is: 1. Generate from the desired `cpflow` release with `--staging-branch master`. 2. Keep generated refs on a release tag once the upstream hardening changes ship. This branch temporarily pins refs to - `9ef104c246670d6c1ea4132dfd22be68ef930a70` to test upstream promotion + `01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` to test upstream promotion hardening before the next release tag. Leave `CPFLOW_VERSION` unset while testing a commit SHA. 3. Keep app names and GitHub settings aligned with `.controlplane/controlplane.yml`. diff --git a/.controlplane/shakacode-team.md b/.controlplane/shakacode-team.md index 358f383e..0911598a 100644 --- a/.controlplane/shakacode-team.md +++ b/.controlplane/shakacode-team.md @@ -118,7 +118,7 @@ Advanced optional settings are documented upstream in the [`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md). Current workflow wrappers are temporarily pinned to upstream -`control-plane-flow` commit `9ef104c246670d6c1ea4132dfd22be68ef930a70` to test +`control-plane-flow` commit `01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` to test promotion hardening before it ships in a release tag. Keep release tags as the steady-state configuration once the upstream PR is released; use a full commit SHA only for short-lived upstream PR testing and leave `CPFLOW_VERSION` unset in diff --git a/.github/cpflow-help.md b/.github/cpflow-help.md index 7112118c..a9f14a61 100644 --- a/.github/cpflow-help.md +++ b/.github/cpflow-help.md @@ -2,7 +2,7 @@ These commands are generated by [cpflow](https://github.com/shakacode/control-plane-flow). For full setup, version-pinning, and troubleshooting details, see the upstream -[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/9ef104c246670d6c1ea4132dfd22be68ef930a70/docs/ci-automation.md). +[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350/docs/ci-automation.md). ## Pull Request Commands @@ -23,11 +23,23 @@ For the normal generated review-app path, GitHub needs one repository secret: | --- | --- | --- | | `CPLN_TOKEN_STAGING` | Repository secret | Control Plane service-account token for the staging/review org. | +For public repositories, use a staging/review token that cannot access +production Control Plane resources. Generated review-app deploys skip fork PR +heads because Docker builds use repository secrets. If a forked change needs a +review app, first move the reviewed change to a trusted branch in this +repository. + No repository variables are required for the standard review-app path when `.controlplane/controlplane.yml` has exactly one review app entry with `match_if_app_name_starts_with: true`. cpflow infers the review-app prefix and staging org from that config. +Review apps run pull request code. Any value mounted through +`cpln://secret/...` can be read by that code after the workload starts, so keep +review-app secret dictionaries limited to disposable databases, review-only +renderer credentials, and license values that are acceptable for review-app +exposure. + Optional overrides exist for forks, clones, and unusual apps: | Name | Notes | @@ -100,11 +112,12 @@ production org, using production-only secrets and values. Generated wrappers normally pin Control Plane Flow with a release tag, for example `v5.1.0`. This branch temporarily pins the wrappers to upstream commit -`9ef104c246670d6c1ea4132dfd22be68ef930a70` while testing unreleased production -promotion hardening. Reusable review-app, staging, cleanup, and helper workflows -pin that ref in their `uses:` entry. Production promotion pins the same ref in -the `Checkout control-plane-flow actions` step so the caller-owned job can keep -`environment: production` and receive production environment secrets directly. +`01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` while testing merged-but-unreleased +production promotion hardening. Reusable review-app, staging, cleanup, and +helper workflows pin that ref in their `uses:` entry. Production promotion pins +the same ref in the `Checkout control-plane-flow actions` step so the +caller-owned job can keep `environment: production` and receive production +environment secrets directly. Leave `CPFLOW_VERSION` unset so the workflow builds cpflow from the same checked-out upstream source. If you set `CPFLOW_VERSION`, it must match the @@ -143,7 +156,7 @@ Most apps do not need these: | Name | Notes | | --- | --- | | `DOCKER_BUILD_EXTRA_ARGS` | Newline-delimited extra Docker build tokens. | -| `DOCKER_BUILD_SSH_KEY` | Private SSH key for Docker builds that fetch private dependencies. | +| `DOCKER_BUILD_SSH_KEY` | Read-only, revocable deploy key for Docker builds that fetch private dependencies. Do not use a personal SSH key. | | `DOCKER_BUILD_SSH_KNOWN_HOSTS` | SSH known_hosts entries when SSH build hosts are not GitHub.com. | | `REVIEW_APP_DEPLOYING_ICON_URL` | Cosmetic custom image URL for the animated deploying icon. Set to `none` to use the text fallback icon. | | `STAGING_APP_BRANCH` | Custom staging branch. The branch must also appear in `cpflow-deploy-staging.yml`'s push filter. | diff --git a/.github/workflows/cpflow-cleanup-stale-review-apps.yml b/.github/workflows/cpflow-cleanup-stale-review-apps.yml index 371ebcb2..f12ce202 100644 --- a/.github/workflows/cpflow-cleanup-stale-review-apps.yml +++ b/.github/workflows/cpflow-cleanup-stale-review-apps.yml @@ -12,6 +12,6 @@ jobs: cleanup: # Cleanup targets the current inferred review-app prefix. If you changed # naming conventions, manually delete review apps under the old prefix. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-delete-review-app.yml b/.github/workflows/cpflow-delete-review-app.yml index c45f3431..5e55afe9 100644 --- a/.github/workflows/cpflow-delete-review-app.yml +++ b/.github/workflows/cpflow-delete-review-app.yml @@ -31,6 +31,6 @@ jobs: github.event_name == 'workflow_dispatch' # This `if:` mirrors the upstream job guard to avoid a billable workflow_call # when the event does not match. Keep both conditions in sync. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-deploy-review-app.yml b/.github/workflows/cpflow-deploy-review-app.yml index 92749113..2c631aca 100644 --- a/.github/workflows/cpflow-deploy-review-app.yml +++ b/.github/workflows/cpflow-deploy-review-app.yml @@ -30,7 +30,7 @@ jobs: github.event.issue.pull_request && contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }} diff --git a/.github/workflows/cpflow-deploy-staging.yml b/.github/workflows/cpflow-deploy-staging.yml index 000a5d50..0c9b1acb 100644 --- a/.github/workflows/cpflow-deploy-staging.yml +++ b/.github/workflows/cpflow-deploy-staging.yml @@ -16,7 +16,7 @@ permissions: jobs: deploy-staging: - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 with: staging_app_branch_default: "master" secrets: diff --git a/.github/workflows/cpflow-help-command.yml b/.github/workflows/cpflow-help-command.yml index 429cfcb7..67e8c214 100644 --- a/.github/workflows/cpflow-help-command.yml +++ b/.github/workflows/cpflow-help-command.yml @@ -23,4 +23,4 @@ jobs: contains(fromJson('["+review-app-help","+review-app-help\n","+review-app-help\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || github.event_name == 'workflow_dispatch' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 diff --git a/.github/workflows/cpflow-promote-staging-to-production.yml b/.github/workflows/cpflow-promote-staging-to-production.yml index 20d8a714..46b03ea2 100644 --- a/.github/workflows/cpflow-promote-staging-to-production.yml +++ b/.github/workflows/cpflow-promote-staging-to-production.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: repository: shakacode/control-plane-flow - ref: 9ef104c246670d6c1ea4132dfd22be68ef930a70 + ref: 01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 path: .cpflow persist-credentials: false @@ -179,7 +179,7 @@ jobs: cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }} cpflow_version: ${{ vars.CPFLOW_VERSION }} # The setup action validates CPFLOW_VERSION against this full workflow ref. - control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 # Runs after Setup production environment so the pinned Ruby (>= 3.1) is on PATH. # YAML.load_file(..., aliases: true) is not supported on Ruby 3.0 (system Ruby on ubuntu-22.04). @@ -307,6 +307,9 @@ jobs: fi } + # check_required_vars intentionally mutates env_check_failed in this + # shell; keep calls outside subshells so failures aggregate before the + # final exit. env_check_failed=0 staging_vars="$(list_gvc_env_names "${CPLN_TOKEN_STAGING}" "${CPLN_ORG_STAGING}" "${STAGING_APP_NAME}")" @@ -467,13 +470,9 @@ jobs: copy_image_attempts=$((copy_image_retries + 1)) copy_image_retry_interval=$((10#${COPY_IMAGE_RETRY_INTERVAL})) - if [[ "${STAGING_IMAGE}" == *@* ]]; then - staging_image="${STAGING_IMAGE}" - else - staging_image="${STAGING_IMAGE%%@*}" - fi + staging_image="${STAGING_IMAGE}" if [[ -z "${staging_image}" ]]; then - echo "::error::Staging image '${STAGING_IMAGE}' did not contain a usable image reference." + echo "::error::STAGING_IMAGE is not set or is empty." exit 1 fi diff --git a/.github/workflows/cpflow-review-app-help.yml b/.github/workflows/cpflow-review-app-help.yml index 4e5ea326..c073ff90 100644 --- a/.github/workflows/cpflow-review-app-help.yml +++ b/.github/workflows/cpflow-review-app-help.yml @@ -18,4 +18,4 @@ jobs: # to PR-open help. Remove it, or uncomment and adapt this guard, if forks or # clones should stay quiet until Control Plane is configured: # if: vars.REVIEW_APP_PREFIX != '' || vars.CPLN_ORG_STAGING != '' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@9ef104c246670d6c1ea4132dfd22be68ef930a70 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 From 0115b6836c3a1a5359e34b545c1dcda7a1632207 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 2 Jun 2026 20:11:54 -1000 Subject: [PATCH 3/3] Pin promotion wrappers to final upstream hardening --- .controlplane/readme.md | 2 +- .controlplane/shakacode-team.md | 2 +- .github/cpflow-help.md | 14 +++++++------- .../workflows/cpflow-cleanup-stale-review-apps.yml | 2 +- .github/workflows/cpflow-delete-review-app.yml | 2 +- .github/workflows/cpflow-deploy-review-app.yml | 2 +- .github/workflows/cpflow-deploy-staging.yml | 2 +- .github/workflows/cpflow-help-command.yml | 2 +- .../cpflow-promote-staging-to-production.yml | 7 +++++-- .github/workflows/cpflow-review-app-help.yml | 2 +- 10 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.controlplane/readme.md b/.controlplane/readme.md index c5c23bfc..f6c25442 100644 --- a/.controlplane/readme.md +++ b/.controlplane/readme.md @@ -577,7 +577,7 @@ For this repo, the update loop is: 1. Generate from the desired `cpflow` release with `--staging-branch master`. 2. Keep generated refs on a release tag once the upstream hardening changes ship. This branch temporarily pins refs to - `01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` to test upstream promotion + `2d8225572edd6f54c83ba9c51bd2983546989e93` to test upstream promotion hardening before the next release tag. Leave `CPFLOW_VERSION` unset while testing a commit SHA. 3. Keep app names and GitHub settings aligned with `.controlplane/controlplane.yml`. diff --git a/.controlplane/shakacode-team.md b/.controlplane/shakacode-team.md index 0911598a..c1206138 100644 --- a/.controlplane/shakacode-team.md +++ b/.controlplane/shakacode-team.md @@ -118,7 +118,7 @@ Advanced optional settings are documented upstream in the [`control-plane-flow` CI automation guide](https://github.com/shakacode/control-plane-flow/blob/main/docs/ci-automation.md). Current workflow wrappers are temporarily pinned to upstream -`control-plane-flow` commit `01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` to test +`control-plane-flow` commit `2d8225572edd6f54c83ba9c51bd2983546989e93` to test promotion hardening before it ships in a release tag. Keep release tags as the steady-state configuration once the upstream PR is released; use a full commit SHA only for short-lived upstream PR testing and leave `CPFLOW_VERSION` unset in diff --git a/.github/cpflow-help.md b/.github/cpflow-help.md index a9f14a61..b05471a3 100644 --- a/.github/cpflow-help.md +++ b/.github/cpflow-help.md @@ -2,7 +2,7 @@ These commands are generated by [cpflow](https://github.com/shakacode/control-plane-flow). For full setup, version-pinning, and troubleshooting details, see the upstream -[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350/docs/ci-automation.md). +[CI automation guide](https://github.com/shakacode/control-plane-flow/blob/2d8225572edd6f54c83ba9c51bd2983546989e93/docs/ci-automation.md). ## Pull Request Commands @@ -112,12 +112,12 @@ production org, using production-only secrets and values. Generated wrappers normally pin Control Plane Flow with a release tag, for example `v5.1.0`. This branch temporarily pins the wrappers to upstream commit -`01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350` while testing merged-but-unreleased -production promotion hardening. Reusable review-app, staging, cleanup, and -helper workflows pin that ref in their `uses:` entry. Production promotion pins -the same ref in the `Checkout control-plane-flow actions` step so the -caller-owned job can keep `environment: production` and receive production -environment secrets directly. +`2d8225572edd6f54c83ba9c51bd2983546989e93` while testing +merged-but-unreleased production promotion hardening. Reusable review-app, +staging, cleanup, and helper workflows pin that ref in their `uses:` entry. +Production promotion pins the same ref in its control-plane-flow checkout step +so the caller-owned job can keep `environment: production` and receive +production environment secrets directly. Leave `CPFLOW_VERSION` unset so the workflow builds cpflow from the same checked-out upstream source. If you set `CPFLOW_VERSION`, it must match the diff --git a/.github/workflows/cpflow-cleanup-stale-review-apps.yml b/.github/workflows/cpflow-cleanup-stale-review-apps.yml index f12ce202..24111068 100644 --- a/.github/workflows/cpflow-cleanup-stale-review-apps.yml +++ b/.github/workflows/cpflow-cleanup-stale-review-apps.yml @@ -12,6 +12,6 @@ jobs: cleanup: # Cleanup targets the current inferred review-app prefix. If you changed # naming conventions, manually delete review apps under the old prefix. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-cleanup-stale-review-apps.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-delete-review-app.yml b/.github/workflows/cpflow-delete-review-app.yml index 5e55afe9..fe94b89a 100644 --- a/.github/workflows/cpflow-delete-review-app.yml +++ b/.github/workflows/cpflow-delete-review-app.yml @@ -31,6 +31,6 @@ jobs: github.event_name == 'workflow_dispatch' # This `if:` mirrors the upstream job guard to avoid a billable workflow_call # when the event does not match. Keep both conditions in sync. - uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-delete-review-app.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} diff --git a/.github/workflows/cpflow-deploy-review-app.yml b/.github/workflows/cpflow-deploy-review-app.yml index 2c631aca..2cffcbd4 100644 --- a/.github/workflows/cpflow-deploy-review-app.yml +++ b/.github/workflows/cpflow-deploy-review-app.yml @@ -30,7 +30,7 @@ jobs: github.event.issue.pull_request && contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }} diff --git a/.github/workflows/cpflow-deploy-staging.yml b/.github/workflows/cpflow-deploy-staging.yml index 0c9b1acb..6521df24 100644 --- a/.github/workflows/cpflow-deploy-staging.yml +++ b/.github/workflows/cpflow-deploy-staging.yml @@ -16,7 +16,7 @@ permissions: jobs: deploy-staging: - uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-staging.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 with: staging_app_branch_default: "master" secrets: diff --git a/.github/workflows/cpflow-help-command.yml b/.github/workflows/cpflow-help-command.yml index 67e8c214..bcecbfd8 100644 --- a/.github/workflows/cpflow-help-command.yml +++ b/.github/workflows/cpflow-help-command.yml @@ -23,4 +23,4 @@ jobs: contains(fromJson('["+review-app-help","+review-app-help\n","+review-app-help\r\n"]'), github.event.comment.body) && contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) || github.event_name == 'workflow_dispatch' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-help-command.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 diff --git a/.github/workflows/cpflow-promote-staging-to-production.yml b/.github/workflows/cpflow-promote-staging-to-production.yml index 46b03ea2..e07a1cc2 100644 --- a/.github/workflows/cpflow-promote-staging-to-production.yml +++ b/.github/workflows/cpflow-promote-staging-to-production.yml @@ -69,7 +69,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: repository: shakacode/control-plane-flow - ref: 01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + ref: 2d8225572edd6f54c83ba9c51bd2983546989e93 path: .cpflow persist-credentials: false @@ -179,7 +179,7 @@ jobs: cpln_cli_version: ${{ vars.CPLN_CLI_VERSION }} cpflow_version: ${{ vars.CPFLOW_VERSION }} # The setup action validates CPFLOW_VERSION against this full workflow ref. - control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + control_plane_flow_ref: shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@2d8225572edd6f54c83ba9c51bd2983546989e93 # Runs after Setup production environment so the pinned Ruby (>= 3.1) is on PATH. # YAML.load_file(..., aliases: true) is not supported on Ruby 3.0 (system Ruby on ubuntu-22.04). @@ -490,10 +490,13 @@ jobs: staging_commit="" if [[ "${staging_tag}" == *_* ]]; then staging_commit="${staging_tag##*_}" + else + echo "::warning::Staging image '${staging_image}' did not include a '_' suffix; production image tag will omit the commit suffix." fi # The workflow-level concurrency group serializes this sequence so two # production promotions cannot derive and publish the same next tag. + # See the top-level concurrency group: cpflow-promote-staging-to-production. latest_number="$( cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" --max 0 -o json | jq -r --arg prefix "${PRODUCTION_APP_NAME}:" \ diff --git a/.github/workflows/cpflow-review-app-help.yml b/.github/workflows/cpflow-review-app-help.yml index c073ff90..5218d197 100644 --- a/.github/workflows/cpflow-review-app-help.yml +++ b/.github/workflows/cpflow-review-app-help.yml @@ -18,4 +18,4 @@ jobs: # to PR-open help. Remove it, or uncomment and adapt this guard, if forks or # clones should stay quiet until Control Plane is configured: # if: vars.REVIEW_APP_PREFIX != '' || vars.CPLN_ORG_STAGING != '' - uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@01dd1d231ce3d8849bcb7ed36b9fd9d184eb3350 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-review-app-help.yml@2d8225572edd6f54c83ba9c51bd2983546989e93