diff --git a/.agents/agent-workflow.yml b/.agents/agent-workflow.yml index e781e476..5558c843 100644 --- a/.agents/agent-workflow.yml +++ b/.agents/agent-workflow.yml @@ -2,6 +2,8 @@ base_branch: master merge_submission: mode: direct +trusted_actions: + - shakacode/control-plane-flow follow_up_prefix: 'Follow-up:' review_gate: n/a approval_exempt: n/a diff --git a/.controlplane/controlplane.yml b/.controlplane/controlplane.yml index d321df04..78612cc2 100644 --- a/.controlplane/controlplane.yml +++ b/.controlplane/controlplane.yml @@ -83,5 +83,10 @@ apps: <<: *common # match_if_app_name_starts_with is used to identify these "qa" apps. match_if_app_name_starts_with: true + # These review-app one-off defaults request app-sized resources and set a + # 15-minute server-side active deadline. + runner_job_default_cpu: "300m" + runner_job_default_memory: "1Gi" + runner_job_timeout: 900 image_retention_days: 5 stale_app_image_deployed_days: 5 # If the app is older than 5 days, the nightly automations will clean stale apps. diff --git a/.github/workflows/cpflow-deploy-review-app.yml b/.github/workflows/cpflow-deploy-review-app.yml index 056ed624..eefa2b28 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.2.0 + uses: shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml@b8676066d3344098bef8e35ee4abce1d0ca9d8a3 # unreleased control-plane-flow PR #435 revision secrets: CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} DOCKER_BUILD_SSH_KEY: ${{ secrets.DOCKER_BUILD_SSH_KEY }} diff --git a/bin/test-cpflow-github-flow b/bin/test-cpflow-github-flow index 8ea97e0b..3750e51c 100755 --- a/bin/test-cpflow-github-flow +++ b/bin/test-cpflow-github-flow @@ -132,8 +132,13 @@ PROMOTE_WORKFLOW = %r{\Ashakacode/control-plane-flow/\.github/workflows/cpflow-p EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT = "shakacode/control-plane-flow/.github/workflows/cpflow-promote-staging-to-production.yml@vX.Y.Z" EXPECTED_CPFLOW_CHECKOUT_ACTION = "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" EXPECTED_CPFLOW_CHECKOUT_REPOSITORY = "shakacode/control-plane-flow" +REVIEW_APP_CANARY_PATH = ".github/workflows/cpflow-deploy-review-app.yml" +REVIEW_APP_CANARY_JOB = "deploy" +REVIEW_APP_CANARY_WORKFLOW = "shakacode/control-plane-flow/.github/workflows/cpflow-deploy-review-app.yml" +FULL_COMMIT_SHA = /\A[0-9a-f]{40}\z/i -refs = Hash.new { |hash, key| hash[key] = [] } +stable_refs = Hash.new { |hash, key| hash[key] = [] } +review_app_canary_entries = [] Dir[".github/workflows/cpflow-*.yml"].sort.each do |path| doc = YAML.load_file(path, aliases: true) @@ -157,7 +162,17 @@ Dir[".github/workflows/cpflow-*.yml"].sort.each do |path| end uses_ref = uses_match[1] - refs[uses_ref] << "#{path}:#{job_name}" + entry = "#{path}:#{job_name}" + if path == REVIEW_APP_CANARY_PATH && job_name == REVIEW_APP_CANARY_JOB + canary_workflow = job["uses"].to_s.delete_suffix("@#{uses_ref}") + unless canary_workflow == REVIEW_APP_CANARY_WORKFLOW + abort "#{entry} must call #{REVIEW_APP_CANARY_WORKFLOW}, got #{canary_workflow.inspect}" + end + + review_app_canary_entries << { ref: uses_ref, entry: entry } + else + stable_refs[uses_ref] << entry + end if job["uses"].to_s.match?(PROMOTE_WORKFLOW) abort "#{path}:#{job_name} must not call the cross-repo production reusable workflow; use a normal caller-repo job with environment: production" @@ -204,7 +219,7 @@ if checkout_ref.to_s.strip.empty? abort "#{promote_path}:promote-to-production must pin the Checkout control-plane-flow actions step" end -refs[checkout_ref] << "#{promote_path}:promote-to-production" +stable_refs[checkout_ref] << "#{promote_path}:promote-to-production" setup_step = Array(promote_job["steps"]).find { |step| step["name"] == "Setup production environment" } @@ -220,17 +235,34 @@ unless setup_match "for example #{EXPECTED_PROMOTE_WORKFLOW_REF_FORMAT}" end -refs[setup_match[1]] << "#{promote_path}:promote-to-production setup" +stable_refs[setup_match[1]] << "#{promote_path}:promote-to-production setup" + +unless review_app_canary_entries.length == 1 + abort "expected exactly one review-app canary entry at #{REVIEW_APP_CANARY_PATH}:#{REVIEW_APP_CANARY_JOB}, " \ + "found #{review_app_canary_entries.length}" +end -if refs.empty? - puts "no upstream cpflow reusable workflow refs found" -elsif refs.length > 1 - refs.each do |ref, paths| +if stable_refs.empty? + abort "no stable upstream cpflow refs found outside the review-app canary" +elsif stable_refs.length > 1 + stable_refs.each do |ref, paths| puts "#{ref}: #{paths.uniq.sort.join(', ')}" end - abort "cpflow workflow wrappers use multiple upstream refs: #{refs.keys.sort.join(', ')}" + abort "non-canary cpflow workflow wrappers use multiple upstream refs: #{stable_refs.keys.sort.join(', ')}" +end + +stable_ref = stable_refs.keys.fetch(0) +review_app_canary = review_app_canary_entries.fetch(0) +review_app_ref = review_app_canary.fetch(:ref) + +if review_app_ref == stable_ref + puts "cpflow refs: #{stable_ref}" +elsif review_app_ref.match?(FULL_COMMIT_SHA) + puts "cpflow stable ref: #{stable_ref}" + puts "review-app canary ref: #{review_app_ref} (#{review_app_canary.fetch(:entry)})" else - puts "cpflow refs: #{refs.keys.sort.join(', ')}" + abort "review-app canary may diverge from #{stable_ref} only with a full 40-character commit SHA, " \ + "got #{review_app_ref.inspect}" end RUBY