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 +4 -4
- data/.agents/agent-workflow.yml +13 -2
- data/.agents/bin/README.md +4 -3
- data/.github/actions/cpflow-setup-environment/action.yml +1 -1
- data/.github/actions/cpflow-wait-for-health/action.yml +87 -15
- data/.github/pull_request_template.md +18 -0
- data/.github/workflows/claude.yml +92 -1
- data/.github/workflows/cpflow-delete-review-app.yml +621 -33
- data/.github/workflows/cpflow-deploy-review-app.yml +640 -20
- data/.github/workflows/cpflow-review-app-help.yml +5 -13
- data/AGENTS.md +6 -49
- data/CHANGELOG.md +33 -1
- data/CONTRIBUTING.md +9 -1
- data/Gemfile.lock +1 -1
- data/docs/ci-automation.md +105 -40
- data/docs/commands.md +19 -2
- data/docs/secrets-and-env-values.md +8 -0
- data/docs/tips.md +18 -0
- data/lib/command/apply_template.rb +104 -2
- data/lib/command/base.rb +52 -3
- data/lib/command/deploy_image.rb +16 -2
- data/lib/command/run.rb +133 -10
- data/lib/command/setup_app.rb +10 -5
- data/lib/core/controlplane.rb +38 -5
- data/lib/core/controlplane_api.rb +8 -0
- data/lib/core/controlplane_api_direct.rb +257 -63
- data/lib/core/shell.rb +9 -2
- data/lib/cpflow/version.rb +1 -1
- data/lib/github_flow_templates/.github/cpflow-help.md +24 -10
- data/lib/github_flow_templates/.github/workflows/cpflow-delete-review-app.yml +10 -0
- data/lib/github_flow_templates/.github/workflows/cpflow-deploy-review-app.yml +9 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd92be5f61c4108f79f0d356ade6d46fc89b637179986563354b874ce50fdac5
|
|
4
|
+
data.tar.gz: 2086b7705d7dfb7f69864cce1a41a380637aa644190a1ed011e0c7d948101205
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35d38fcb4bd0fdaf2f8334daf3b36631a1ededcaf0b5e9fea3b4da15c968ba34db8c14415b2ff84f1bb5701b630d96fe531b1c4e0dac3ae88f171c62796ecb64
|
|
7
|
+
data.tar.gz: dd1ba11c088bd4190dea8995cc750499e83f2c3520cea1fdc88f01678187ce107e27f12425ebf378972c77f0f5aaf9f6f83eee9d257b2eabbdcd4aeb8f7f12f8
|
data/.agents/agent-workflow.yml
CHANGED
|
@@ -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
|
data/.agents/bin/README.md
CHANGED
|
@@ -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
|
|
18
|
-
[`../agent-workflow.yml`](../agent-workflow.yml)
|
|
19
|
-
|
|
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.
|
|
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
|
|
5
|
-
is in the accepted list. Fails non-zero
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|