diff --git a/.github/workflows/cpflow-promote-staging-to-production.yml b/.github/workflows/cpflow-promote-staging-to-production.yml index a6fb9fca..c57693a5 100644 --- a/.github/workflows/cpflow-promote-staging-to-production.yml +++ b/.github/workflows/cpflow-promote-staging-to-production.yml @@ -290,6 +290,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}")" @@ -448,13 +451,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/lib/github_flow_templates/.github/cpflow-help.md b/lib/github_flow_templates/.github/cpflow-help.md index 1da64075..58cc7c84 100644 --- a/lib/github_flow_templates/.github/cpflow-help.md +++ b/lib/github_flow_templates/.github/cpflow-help.md @@ -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 | @@ -142,7 +154,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/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml b/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml index 7ca25bdf..f7ab170c 100644 --- a/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml +++ b/lib/github_flow_templates/.github/workflows/cpflow-promote-staging-to-production.yml @@ -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/spec/command/generate_github_actions_spec.rb b/spec/command/generate_github_actions_spec.rb index 4086ccf9..1b5f54bf 100644 --- a/spec/command/generate_github_actions_spec.rb +++ b/spec/command/generate_github_actions_spec.rb @@ -692,6 +692,12 @@ def shared_workflow_path(name) expect(help_md).to include("Before the first staging deploy") expect(help_md).to include("cpflow setup-app -a") expect(help_md).to include("app secret policy") + expect(help_md).to include("For public repositories, use a staging/review token") + expect(help_md).to include("production Control Plane resources") + expect(help_md).to include("Generated review-app deploys skip fork PR") + expect(help_md).to include("heads because Docker builds use repository secrets") + expect(help_md).to include("Review apps run pull request code") + expect(help_md).to include("review-app secret dictionaries limited to disposable databases") expect(help_md).to include("Add it as a secret on the 'production' GitHub Environment") expect(help_md).to include("permission to manage repository environments and secrets") expect(help_md).to include("gh secret set CPLN_TOKEN_PRODUCTION --repo OWNER/REPO --env production") @@ -703,6 +709,8 @@ def shared_workflow_path(name) help_md_path = playground.join(".github/cpflow-help.md") contents = help_md_path.read expect(contents).to include("DOCKER_BUILD_EXTRA_ARGS") + expect(contents).to include("Read-only, revocable deploy key") + expect(contents).to include("Do not use a personal SSH key") expect(contents).to include("DOCKER_BUILD_SSH_KNOWN_HOSTS") end @@ -794,11 +802,13 @@ def shared_workflow_path(name) expect(contents).to include("Production-only variables") expect(contents).to include("WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }}") expect(contents).to include("list_workload_env_names()") + expect(contents).to include("check_required_vars intentionally mutates env_check_failed") expect(contents).to include( "Production workload '${workload_name}' is missing environment variables that exist in staging" ) expect(wrapper).to include("WORKLOAD_NAMES: ${{ steps.workloads.outputs.names }}") expect(wrapper).to include("list_workload_env_names()") + expect(wrapper).to include("check_required_vars intentionally mutates env_check_failed") expect(wrapper).to include( "Production workload '${workload_name}' is missing environment variables that exist in staging" ) @@ -859,10 +869,9 @@ def shared_workflow_path(name) "uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5" ) expect(contents).to include("id: copy-image") - expect(contents).to include('if [[ "${STAGING_IMAGE}" == *@* ]]; then') expect(contents).to include('staging_image="${STAGING_IMAGE}"') - expect(contents).to include("else") - expect(contents).to include('staging_image="${STAGING_IMAGE%%@*}"') + expect(contents).to include("STAGING_IMAGE is not set or is empty") + expect(contents).not_to include('staging_image="${STAGING_IMAGE%%@*}"') expect(contents).to include('CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image get "${staging_image}"') expect(contents).to include('if [[ "${staging_image}" == *@* ]]; then') expect(contents).to include('staging_tag="${staging_image##*@}"')