cpflow 5.2.0 → 5.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d334d4f24d777236b1d99232c0aeb14aaac17a3daed74837738773a5aa42b3c
4
- data.tar.gz: ca7b66e6bd876a6a3f422a425aa86ce90e74af5f57e23eada8384c3f1e963f75
3
+ metadata.gz: cd92be5f61c4108f79f0d356ade6d46fc89b637179986563354b874ce50fdac5
4
+ data.tar.gz: 2086b7705d7dfb7f69864cce1a41a380637aa644190a1ed011e0c7d948101205
5
5
  SHA512:
6
- metadata.gz: e72015fc6143701ee31e311030a71c91957556aba0cc762c01d63834cd71d01ba7bfe4e76e216700a8367e365fbb3da2459c70707e6364e7985326e00e6ccab5
7
- data.tar.gz: 4b19772575225b90d96ebe788cc555699210df4aa9ae92677e6fbde395f4bcc92b2ef2206b062049c8192ca91228e45e7c7c9414b1948b896248758eb8c71370
6
+ metadata.gz: 35d38fcb4bd0fdaf2f8334daf3b36631a1ededcaf0b5e9fea3b4da15c968ba34db8c14415b2ff84f1bb5701b630d96fe531b1c4e0dac3ae88f171c62796ecb64
7
+ data.tar.gz: dd1ba11c088bd4190dea8995cc750499e83f2c3520cea1fdc88f01678187ce107e27f12425ebf378972c77f0f5aaf9f6f83eee9d257b2eabbdcd4aeb8f7f12f8
@@ -1,8 +1,10 @@
1
1
  # Non-command agent-workflow configuration for portable shared skills.
2
+ # This file is the canonical source for the non-command policy values below.
3
+ # AGENTS.md directs portable skills here; it does not duplicate or override them.
2
4
  # Commands live as scripts in .agents/bin/ (see .agents/bin/README.md).
3
- # Compatibility summary for older workflow copies; canonical values live in
4
- # AGENTS.md.
5
5
  base_branch: main
6
+ merge_submission:
7
+ mode: direct
6
8
  changelog: "CHANGELOG.md — Keep-a-Changelog; user-visible changes only"
7
9
  follow_up_prefix: "Follow-up:"
8
10
  review_gate: "AI reviewers are advisory unless they confirm a blocker; merge gate is the full `gh pr checks` list green (not --required) + all threads resolved + mergeable clean"
@@ -11,5 +13,14 @@ coordination_backend: "private shakacode/agent-coordination (claims/heartbeats n
11
13
  benchmark_labels: n/a
12
14
  merge_ledger: n/a
13
15
  ci_parity_environment: "n/a — reproduce CI-only failures from the matching job in .github/workflows/**"
16
+ release_qa_runbook: ".agents/workflows/ai-rollout-e2e-test.md — use after publishing a cpflow gem that changes GitHub Actions, AI rollout prompts, readiness checks, generator output, or React on Rails deployment behavior"
14
17
  hosted_ci_trigger: "n/a — CI runs on every PR"
15
18
  ci_change_detector: n/a
19
+
20
+ # Consumed by the portable untrusted-contributor-intake skill. These values must
21
+ # be read from the trusted base ref, never from contributor-controlled PR content.
22
+ untrusted_contributor_intake:
23
+ trusted_github_host: github.com
24
+ trusted_github_scheme: https
25
+ trusted_github_repo: shakacode/control-plane-flow
26
+ repo_prefix: CPF
@@ -14,6 +14,7 @@ means that capability is n/a here.
14
14
  | `docs` | Check generated command docs | `bundle exec rake check_command_docs` |
15
15
  | `build` | Build / type-check | n/a (gem) |
16
16
 
17
- Canonical non-command policy lives in [`../../AGENTS.md`](../../AGENTS.md).
18
- [`../agent-workflow.yml`](../agent-workflow.yml) is retained only as a
19
- compatibility summary for older workflow copies.
17
+ Canonical non-command policy, including the release-QA runbook reference, lives
18
+ in [`../agent-workflow.yml`](../agent-workflow.yml). [`../../AGENTS.md`](../../AGENTS.md)
19
+ is the thin discovery pointer for portable shared skills and does not duplicate
20
+ or override that policy.
@@ -99,7 +99,7 @@ runs:
99
99
 
100
100
  # Bump this default when a new Control Plane CLI release should roll out by default.
101
101
  # Override per-repo by setting the `CPLN_CLI_VERSION` repo variable.
102
- default_cpln_cli_version="3.10.2"
102
+ default_cpln_cli_version="3.11.0"
103
103
 
104
104
  CPLN_CLI_VERSION="${CPLN_CLI_VERSION:-${default_cpln_cli_version}}"
105
105
 
@@ -1,9 +1,9 @@
1
1
  name: Wait for Control Plane workload health
2
2
  description: >-
3
3
  Polls Control Plane until the latest workload version is ready, then checks
4
- the workload endpoint with curl. Exits success when the HTTP response status
5
- is in the accepted list. Fails non-zero (and reports `healthy=false`) once
6
- retries are exhausted.
4
+ the standard workload endpoint and ready location endpoints with curl. Exits
5
+ success when the HTTP response status is in the accepted list. Fails non-zero
6
+ (and reports `healthy=false`) once retries are exhausted.
7
7
 
8
8
  inputs:
9
9
  workload_name:
@@ -39,6 +39,9 @@ outputs:
39
39
  healthy:
40
40
  description: '"true" once a healthy response was observed; "false" otherwise.'
41
41
  value: ${{ steps.poll.outputs.healthy }}
42
+ endpoint:
43
+ description: The workload or location endpoint that returned an accepted status.
44
+ value: ${{ steps.poll.outputs.endpoint }}
42
45
 
43
46
  runs:
44
47
  using: composite
@@ -59,6 +62,40 @@ runs:
59
62
 
60
63
  read -r -a accepted_statuses <<< "${CPFLOW_ACCEPTED_STATUSES}"
61
64
 
65
+ check_endpoint() {
66
+ local endpoint="$1"
67
+ local endpoint_type="$2"
68
+ local http_status
69
+
70
+ http_status="$(curl -s -o /dev/null -w '%{http_code}' --max-time "${CPFLOW_CURL_MAX_TIME}" "${endpoint}" 2>/dev/null || true)"
71
+ http_status="${http_status:-000}"
72
+ echo "${endpoint_type}: ${endpoint}, HTTP status: ${http_status}"
73
+
74
+ for accepted in "${accepted_statuses[@]}"; do
75
+ if [[ "${http_status}" == "${accepted}" ]]; then
76
+ {
77
+ echo "healthy=true"
78
+ echo "endpoint=${endpoint}"
79
+ } >> "$GITHUB_OUTPUT"
80
+ return 0
81
+ fi
82
+ done
83
+
84
+ return 1
85
+ }
86
+
87
+ summarize_cpln_error() {
88
+ local error_text="$1"
89
+
90
+ case "${error_text}" in
91
+ *401*|*403*|*[Uu]nauthorized*|*[Ff]orbidden*) echo "authorization failure" ;;
92
+ *404*|*"not found"*|*"Not Found"*) echo "resource not found" ;;
93
+ *429*|*"rate limit"*|*"Rate Limit"*) echo "rate limited" ;;
94
+ *"timed out"*|*[Tt]imeout*) echo "request timeout" ;;
95
+ *) echo "CLI error (details suppressed; ${#error_text} stderr bytes captured)" ;;
96
+ esac
97
+ }
98
+
62
99
  for attempt in $(seq 1 "${CPFLOW_MAX_RETRIES}"); do
63
100
  echo "Health check attempt ${attempt}/${CPFLOW_MAX_RETRIES}"
64
101
 
@@ -72,22 +109,57 @@ runs:
72
109
  workload_ready="$(echo "${workload_json}" | jq -r '.status.ready // false')"
73
110
  latest_ready="$(echo "${workload_json}" | jq -r '.status.readyLatest // false')"
74
111
  readiness_status="$(echo "${workload_json}" | jq -r '.health.readiness // "unknown"')"
75
- endpoint="$(echo "${workload_json}" | jq -r '.status.endpoint // empty')"
112
+ workload_endpoint="$(echo "${workload_json}" | jq -r '.status.endpoint // empty')"
76
113
 
77
114
  if [[ "${workload_ready}" != "true" || "${latest_ready}" != "true" ]]; then
78
115
  echo "Workload status: ready=${workload_ready}, readyLatest=${latest_ready}, readiness=${readiness_status}; waiting for latest deployment."
79
- elif [[ -n "${endpoint}" ]]; then
80
- http_status="$(curl -s -o /dev/null -w '%{http_code}' --max-time "${CPFLOW_CURL_MAX_TIME}" "${endpoint}" 2>/dev/null || echo 000)"
81
- echo "Endpoint: ${endpoint}, HTTP status: ${http_status}"
82
-
83
- for accepted in "${accepted_statuses[@]}"; do
84
- if [[ "${http_status}" == "${accepted}" ]]; then
85
- echo "healthy=true" >> "$GITHUB_OUTPUT"
86
- exit 0
87
- fi
88
- done
89
116
  else
90
- echo "Workload '${CPFLOW_WORKLOAD_NAME}' has no endpoint yet; waiting for one to be assigned."
117
+ if [[ -n "${workload_endpoint}" ]] && check_endpoint "${workload_endpoint}" "Workload endpoint"; then
118
+ exit 0
119
+ fi
120
+
121
+ location_endpoint_count=0
122
+ deployments_listed=false
123
+ deployments_settled=false
124
+ deployments_stderr="$(mktemp)"
125
+ if deployments_json="$(cpln workload get-deployments "${CPFLOW_WORKLOAD_NAME}" --gvc "${CPFLOW_APP_NAME}" --org "${CPFLOW_ORG}" -o json 2>"${deployments_stderr}")"; then
126
+ deployments_listed=true
127
+ unsettled_location_count="$(
128
+ printf '%s\n' "${deployments_json}" |
129
+ jq '[.items[]? | select(.status.ready != true or (.status.deploying // false) == true)] | length'
130
+ )"
131
+
132
+ if [[ "${unsettled_location_count}" -gt 0 ]]; then
133
+ echo "Deployment locations are not all ready (${unsettled_location_count} unsettled); waiting before endpoint fallback."
134
+ else
135
+ deployments_settled=true
136
+ while IFS= read -r location_endpoint; do
137
+ location_endpoint_count=$((location_endpoint_count + 1))
138
+ if [[ -n "${location_endpoint}" &&
139
+ "${location_endpoint}" != "${workload_endpoint}" ]] &&
140
+ check_endpoint "${location_endpoint}" "Location endpoint"; then
141
+ rm -f "${deployments_stderr}"
142
+ exit 0
143
+ fi
144
+ done < <(
145
+ printf '%s\n' "${deployments_json}" |
146
+ jq -r '.items[]? | .status.endpoint // empty' |
147
+ awk '!seen[$0]++'
148
+ )
149
+ fi
150
+ else
151
+ deployments_error="$(<"${deployments_stderr}")"
152
+ deployments_error_summary="$(summarize_cpln_error "${deployments_error}")"
153
+ echo "::warning::Could not list location endpoints for workload '${CPFLOW_WORKLOAD_NAME}' (${deployments_error_summary}); retrying."
154
+ fi
155
+ rm -f "${deployments_stderr}"
156
+
157
+ if [[ -z "${workload_endpoint}" &&
158
+ "${deployments_listed}" == "true" &&
159
+ "${deployments_settled}" == "true" &&
160
+ "${location_endpoint_count}" -eq 0 ]]; then
161
+ echo "Workload '${CPFLOW_WORKLOAD_NAME}' has no endpoint yet; waiting for one to be assigned."
162
+ fi
91
163
  fi
92
164
 
93
165
  if [[ "${attempt}" -lt "${CPFLOW_MAX_RETRIES}" ]]; then
@@ -0,0 +1,18 @@
1
+ ## Summary
2
+
3
+ <!-- What changes, and why is it useful? -->
4
+
5
+ ## Related issue
6
+
7
+ <!-- Use "Closes #123" for an accepted issue, or link the maintainer discussion. -->
8
+
9
+ ## Validation
10
+
11
+ <!-- List the exact commands or manual checks you ran. -->
12
+
13
+ ## Checklist
14
+
15
+ - [ ] This PR addresses an accepted issue or maintainer request, or is a small typo fix.
16
+ - [ ] The change is focused and contains no unrelated cleanup or generated churn.
17
+ - [ ] I added or updated tests, documentation, and `CHANGELOG.md` where applicable.
18
+ - [ ] I reviewed and understand all submitted content, including AI-assisted changes.
@@ -11,13 +11,104 @@ on:
11
11
  types: [submitted]
12
12
 
13
13
  jobs:
14
- claude:
14
+ authorize_claude_actor:
15
15
  if: |
16
16
  (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17
17
  (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18
18
  (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19
19
  (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20
20
  runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ outputs:
24
+ authorized: ${{ steps.permission-gate.outputs.authorized }}
25
+ steps:
26
+ - name: Verify actor can run Claude
27
+ id: permission-gate
28
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
29
+ with:
30
+ script: |
31
+ const owner = context.repo.owner;
32
+ const repo = context.repo.repo;
33
+ const permissionByLogin = new Map();
34
+
35
+ function claudeRequester() {
36
+ if (
37
+ context.eventName === 'issue_comment' ||
38
+ context.eventName === 'pull_request_review_comment'
39
+ ) {
40
+ return context.payload.comment?.user?.login;
41
+ }
42
+
43
+ if (context.eventName === 'pull_request_review') {
44
+ return context.payload.review?.user?.login;
45
+ }
46
+
47
+ if (context.eventName === 'issues') {
48
+ return context.payload.issue?.user?.login;
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ async function hasWriteAccessFor(username) {
55
+ if (!username) {
56
+ return false;
57
+ }
58
+
59
+ if (permissionByLogin.has(username)) {
60
+ return permissionByLogin.get(username);
61
+ }
62
+
63
+ let hasWriteAccess = false;
64
+ try {
65
+ const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
66
+ owner,
67
+ repo,
68
+ username
69
+ });
70
+ hasWriteAccess = ['admin', 'write'].includes(permission.permission);
71
+ } catch (error) {
72
+ core.warning(
73
+ `Unable to verify ${username} write permission for Claude; ` +
74
+ `treating the Claude request as untrusted: ${error.message || error}`
75
+ );
76
+ }
77
+
78
+ permissionByLogin.set(username, hasWriteAccess);
79
+ return hasWriteAccess;
80
+ }
81
+
82
+ const actor = context.actor;
83
+ const requester = claudeRequester();
84
+
85
+ if (!requester) {
86
+ core.setOutput('authorized', 'false');
87
+ core.setFailed('Unable to run Claude because the requester could not be determined.');
88
+ return;
89
+ }
90
+
91
+ if (!(await hasWriteAccessFor(actor))) {
92
+ core.setOutput('authorized', 'false');
93
+ core.setFailed(`Unable to run Claude because ${actor} does not have write/admin repository permission.`);
94
+ return;
95
+ }
96
+
97
+ if (!(await hasWriteAccessFor(requester))) {
98
+ core.setOutput('authorized', 'false');
99
+ core.setFailed(
100
+ `Unable to run Claude because requester ${requester} does not have write/admin repository permission.`
101
+ );
102
+ return;
103
+ }
104
+
105
+ core.setOutput('authorized', 'true');
106
+ core.info(`Authorized ${actor} and Claude requester ${requester} to run Claude.`);
107
+
108
+ claude:
109
+ needs: authorize_claude_actor
110
+ if: needs.authorize_claude_actor.outputs.authorized == 'true'
111
+ runs-on: ubuntu-latest
21
112
  permissions:
22
113
  contents: read
23
114
  pull-requests: read