cpflow 5.1.1 → 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 +26 -0
- data/.agents/bin/README.md +20 -0
- data/.agents/bin/docs +5 -0
- data/.agents/bin/lint +5 -0
- data/.agents/bin/setup +5 -0
- data/.agents/bin/test +5 -0
- data/.agents/bin/validate +5 -0
- data/.agents/trusted-github-actors.yml +32 -0
- data/.agents/workflows/ai-rollout-e2e-test.md +166 -0
- 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-code-review.yml +2 -0
- data/.github/workflows/claude.yml +94 -1
- data/.github/workflows/cpflow-delete-review-app.yml +621 -33
- data/.github/workflows/cpflow-deploy-review-app.yml +656 -21
- data/.github/workflows/cpflow-review-app-help.yml +5 -13
- data/.github/workflows/rspec-shared.yml +10 -3
- data/.github/workflows/rspec-specific.yml +1 -0
- data/.github/workflows/rspec.yml +58 -1
- data/AGENTS.md +14 -0
- data/CHANGELOG.md +54 -1
- data/CLAUDE.md +3 -0
- data/CONTRIBUTING.md +15 -3
- data/Gemfile.lock +1 -1
- data/README.md +21 -7
- data/docs/ai-github-flow-prompt.md +18 -16
- data/docs/ci-automation.md +239 -27
- data/docs/commands.md +30 -2
- data/docs/grafana-opentelemetry.md +699 -0
- data/docs/secrets-and-env-values.md +37 -2
- data/docs/sidebars.ts +70 -0
- data/docs/telemetry/application-instrumentation.md +161 -0
- data/docs/telemetry/collector.md +297 -0
- data/docs/telemetry/index.md +152 -0
- data/docs/telemetry/pipelines.md +98 -0
- data/docs/telemetry/review-apps.md +55 -0
- data/docs/telemetry/troubleshooting.md +92 -0
- data/docs/terraform/example/.controlplane/controlplane.yml +0 -1
- data/docs/terraform/overview.md +11 -0
- data/docs/tips.md +475 -28
- data/examples/controlplane.yml +2 -0
- data/lib/command/ai_github_flow_prompt.rb +2 -2
- data/lib/command/apply_template.rb +104 -2
- data/lib/command/base.rb +69 -5
- data/lib/command/deploy_image.rb +93 -7
- data/lib/command/promote_app_from_upstream.rb +1 -0
- data/lib/command/ps_wait.rb +2 -10
- data/lib/command/run.rb +133 -10
- data/lib/command/setup_app.rb +10 -5
- data/lib/core/config.rb +94 -0
- 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/doctor_service.rb +44 -3
- data/lib/core/shell.rb +9 -2
- data/lib/core/template_parser.rb +43 -9
- data/lib/cpflow/version.rb +1 -1
- data/lib/generator_templates/controlplane.yml +1 -2
- data/lib/github_flow_templates/.github/cpflow-help.md +34 -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 +22 -2
|
@@ -4,6 +4,10 @@ run-name: "Deploy Review App - PR #${{ github.event.pull_request.number || githu
|
|
|
4
4
|
|
|
5
5
|
on:
|
|
6
6
|
workflow_call:
|
|
7
|
+
outputs:
|
|
8
|
+
image_built:
|
|
9
|
+
description: Whether this run successfully built the application image.
|
|
10
|
+
value: ${{ jobs.deploy.outputs.image_built }}
|
|
7
11
|
secrets:
|
|
8
12
|
CPLN_TOKEN_STAGING:
|
|
9
13
|
description: Control Plane token for the staging org that owns review apps.
|
|
@@ -13,40 +17,576 @@ on:
|
|
|
13
17
|
required: false
|
|
14
18
|
|
|
15
19
|
permissions:
|
|
20
|
+
actions: write
|
|
16
21
|
contents: read
|
|
17
22
|
deployments: write
|
|
18
23
|
issues: write
|
|
19
24
|
pull-requests: write
|
|
20
25
|
|
|
21
|
-
concurrency:
|
|
22
|
-
group: cpflow-review-app-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
|
23
|
-
# Match the delete workflow: a cancelled `cpflow deploy-image` mid-rollout can leave the
|
|
24
|
-
# review app in a partially-deployed state (workload update in progress, rollout not
|
|
25
|
-
# settled). Let an in-flight deploy finish before the next push starts a new run.
|
|
26
|
-
cancel-in-progress: false
|
|
27
|
-
|
|
28
26
|
env:
|
|
29
27
|
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
|
30
28
|
PRIMARY_WORKLOAD: ${{ vars.PRIMARY_WORKLOAD }}
|
|
31
29
|
|
|
32
30
|
jobs:
|
|
31
|
+
authorize-comment-command:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
timeout-minutes: 5
|
|
34
|
+
permissions:
|
|
35
|
+
actions: read
|
|
36
|
+
# PR issue comments use the Issues API path but require PR write access.
|
|
37
|
+
pull-requests: write
|
|
38
|
+
outputs:
|
|
39
|
+
allowed: ${{ steps.record.outputs.accepted || steps.intent.outputs.reuse }}
|
|
40
|
+
steps:
|
|
41
|
+
- name: Resolve comment author repository permission
|
|
42
|
+
if: github.event_name == 'issue_comment'
|
|
43
|
+
id: permission
|
|
44
|
+
shell: bash
|
|
45
|
+
env:
|
|
46
|
+
GH_TOKEN: ${{ github.token }}
|
|
47
|
+
GH_REPO: ${{ github.repository }}
|
|
48
|
+
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
|
|
49
|
+
run: |
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
|
|
52
|
+
printf '%s\n' 'allowed=false' >> "$GITHUB_OUTPUT"
|
|
53
|
+
permission="$(gh api --method GET "repos/${GH_REPO}/collaborators/${COMMENT_AUTHOR}/permission" --jq '.permission')"
|
|
54
|
+
|
|
55
|
+
if [[ -z "${permission}" ]]; then
|
|
56
|
+
echo "GitHub returned an empty repository permission." >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
case "${permission}" in
|
|
61
|
+
write|maintain|admin)
|
|
62
|
+
printf '%s\n' 'allowed=true' >> "$GITHUB_OUTPUT"
|
|
63
|
+
;;
|
|
64
|
+
read|triage|none)
|
|
65
|
+
echo "Comment author repository permission does not allow review-app commands: ${permission}"
|
|
66
|
+
;;
|
|
67
|
+
*)
|
|
68
|
+
echo "Unexpected repository permission: ${permission}" >&2
|
|
69
|
+
exit 1
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
|
|
73
|
+
- name: Allow authenticated non-comment trigger
|
|
74
|
+
if: github.event_name != 'issue_comment'
|
|
75
|
+
id: non-comment
|
|
76
|
+
shell: bash
|
|
77
|
+
run: echo "allowed=true" >> "$GITHUB_OUTPUT"
|
|
78
|
+
|
|
79
|
+
# Persist a durable accepted-event marker before queue admission. The
|
|
80
|
+
# original comment may later be edited/deleted, and authorization jobs can
|
|
81
|
+
# finish out of event order, so the marker carries the workflow run's own
|
|
82
|
+
# creation time and id rather than relying on marker creation order.
|
|
83
|
+
- name: Prepare accepted review app intent
|
|
84
|
+
id: intent
|
|
85
|
+
if: |
|
|
86
|
+
(steps.permission.outputs.allowed == 'true' || steps.non-comment.outputs.allowed == 'true') &&
|
|
87
|
+
((github.event_name == 'issue_comment' &&
|
|
88
|
+
github.event.issue.pull_request &&
|
|
89
|
+
contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body)) ||
|
|
90
|
+
(github.event_name == 'pull_request' &&
|
|
91
|
+
contains(fromJson('["opened","synchronize","reopened"]'), github.event.action)) ||
|
|
92
|
+
github.event_name == 'workflow_dispatch')
|
|
93
|
+
shell: bash
|
|
94
|
+
env:
|
|
95
|
+
GH_TOKEN: ${{ github.token }}
|
|
96
|
+
GH_REPO: ${{ github.repository }}
|
|
97
|
+
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
|
98
|
+
OPERATION: deploy
|
|
99
|
+
EVENT_NAME: ${{ github.event_name }}
|
|
100
|
+
SOURCE_ACTOR: ${{ github.event.comment.user.login || github.actor }}
|
|
101
|
+
RUN_ID: ${{ github.run_id }}
|
|
102
|
+
RECONCILE_INTENT_RUN_ID: ${{ github.event.inputs.reconcile_intent_run_id }}
|
|
103
|
+
run: |
|
|
104
|
+
set -euo pipefail
|
|
105
|
+
|
|
106
|
+
printf '%s\n' 'record=false' 'reuse=false' >> "$GITHUB_OUTPUT"
|
|
107
|
+
if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ || ! "${RUN_ID}" =~ ^[0-9]+$ ]]; then
|
|
108
|
+
echo "Invalid pull request or workflow run id while recording review-app intent." >&2
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
case "${OPERATION}" in
|
|
112
|
+
deploy|delete) ;;
|
|
113
|
+
*) echo "Invalid review-app operation: ${OPERATION}" >&2; exit 1 ;;
|
|
114
|
+
esac
|
|
115
|
+
case "${EVENT_NAME}" in
|
|
116
|
+
issue_comment|workflow_dispatch|pull_request|pull_request_target) ;;
|
|
117
|
+
*) echo "Invalid review-app event: ${EVENT_NAME}" >&2; exit 1 ;;
|
|
118
|
+
esac
|
|
119
|
+
if [[ -z "${SOURCE_ACTOR}" ]]; then
|
|
120
|
+
echo "Review-app intent is missing its source actor." >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
pull_request_json="$(
|
|
125
|
+
gh api --method GET "repos/${GH_REPO}/pulls/${PR_NUMBER}"
|
|
126
|
+
)"
|
|
127
|
+
head_repository="$(jq -r '.head.repo.full_name // empty' <<< "${pull_request_json}")"
|
|
128
|
+
pull_request_state="$(jq -r '.state // empty' <<< "${pull_request_json}")"
|
|
129
|
+
case "${pull_request_state}" in
|
|
130
|
+
open) ;;
|
|
131
|
+
closed)
|
|
132
|
+
echo "Refusing review-app deploy intent because PR #${PR_NUMBER} is closed."
|
|
133
|
+
exit 0
|
|
134
|
+
;;
|
|
135
|
+
"")
|
|
136
|
+
echo "GitHub returned an empty pull request state while preparing review-app deploy intent." >&2
|
|
137
|
+
exit 1
|
|
138
|
+
;;
|
|
139
|
+
*)
|
|
140
|
+
echo "Unexpected pull request state while preparing review-app deploy intent: ${pull_request_state}" >&2
|
|
141
|
+
exit 1
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
if [[ -z "${head_repository}" ]]; then
|
|
145
|
+
echo "GitHub returned an empty head repository while preparing review-app deploy intent." >&2
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
if [[ "${head_repository}" != "${GH_REPO}" ]]; then
|
|
149
|
+
echo "Review app deploys are skipped for fork pull requests."
|
|
150
|
+
exit 0
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
if [[ -n "${RECONCILE_INTENT_RUN_ID}" ]]; then
|
|
154
|
+
if [[ ! "${RECONCILE_INTENT_RUN_ID}" =~ ^[0-9]+$ ]]; then
|
|
155
|
+
echo "Invalid internal review-app sequencing token." >&2
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
handoff=""
|
|
160
|
+
for _attempt in {1..10}; do
|
|
161
|
+
comments_json="$(
|
|
162
|
+
gh api --paginate --slurp --method GET \
|
|
163
|
+
"repos/${GH_REPO}/issues/${PR_NUMBER}/comments?per_page=100"
|
|
164
|
+
)"
|
|
165
|
+
handoff="$(
|
|
166
|
+
jq -c \
|
|
167
|
+
--argjson pr "${PR_NUMBER}" \
|
|
168
|
+
--arg operation "${OPERATION}" \
|
|
169
|
+
--argjson intent_run_id "${RECONCILE_INTENT_RUN_ID}" \
|
|
170
|
+
--argjson dispatch_run_id "${RUN_ID}" '
|
|
171
|
+
[
|
|
172
|
+
.[][]?
|
|
173
|
+
| select(.user.login == "github-actions[bot]")
|
|
174
|
+
| .body
|
|
175
|
+
| select(type == "string")
|
|
176
|
+
| select(startswith("<!-- cpflow-review-app-redispatch-v1 ") and endswith(" -->"))
|
|
177
|
+
| ltrimstr("<!-- cpflow-review-app-redispatch-v1 ")
|
|
178
|
+
| rtrimstr(" -->")
|
|
179
|
+
| fromjson?
|
|
180
|
+
| select(
|
|
181
|
+
.pr == $pr and
|
|
182
|
+
.operation == $operation and
|
|
183
|
+
.intent_run_id == $intent_run_id and
|
|
184
|
+
.dispatch_run_id == $dispatch_run_id
|
|
185
|
+
)
|
|
186
|
+
| select(
|
|
187
|
+
(.source_run_id | type) == "number" and
|
|
188
|
+
.source_run_id > 0 and
|
|
189
|
+
(.source_run_id | floor) == .source_run_id
|
|
190
|
+
)
|
|
191
|
+
| select(
|
|
192
|
+
(.created_at | type) == "string" and
|
|
193
|
+
(.created_at | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
|
|
194
|
+
)
|
|
195
|
+
]
|
|
196
|
+
| if length == 1 then .[0] else empty end
|
|
197
|
+
' <<< "${comments_json}"
|
|
198
|
+
)"
|
|
199
|
+
[[ -n "${handoff}" ]] && break
|
|
200
|
+
sleep 1
|
|
201
|
+
done
|
|
202
|
+
if [[ -z "${handoff}" ]]; then
|
|
203
|
+
echo "Internal review-app sequencing token is not bound to this workflow run." >&2
|
|
204
|
+
exit 1
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
source_run_id="$(jq -r '.source_run_id' <<< "${handoff}")"
|
|
208
|
+
source_created_at="$(jq -r '.created_at' <<< "${handoff}")"
|
|
209
|
+
case "${OPERATION}" in
|
|
210
|
+
deploy)
|
|
211
|
+
source_operation="delete"
|
|
212
|
+
expected_run_name="Delete Review App - PR #${PR_NUMBER}"
|
|
213
|
+
expected_job_name="delete-review-app / delete-review-app"
|
|
214
|
+
;;
|
|
215
|
+
delete)
|
|
216
|
+
source_operation="deploy"
|
|
217
|
+
expected_run_name="Deploy Review App - PR #${PR_NUMBER}"
|
|
218
|
+
expected_job_name="deploy / deploy"
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
expected_workflow_path=".github/workflows/cpflow-${source_operation}-review-app.yml"
|
|
222
|
+
source_run_json="$(
|
|
223
|
+
gh api --method GET "repos/${GH_REPO}/actions/runs/${source_run_id}"
|
|
224
|
+
)"
|
|
225
|
+
if ! jq -e \
|
|
226
|
+
--argjson run_id "${source_run_id}" \
|
|
227
|
+
--arg created_at "${source_created_at}" \
|
|
228
|
+
--arg path "${expected_workflow_path}" \
|
|
229
|
+
--arg title "${expected_run_name}" \
|
|
230
|
+
'(.id == $run_id) and
|
|
231
|
+
(.created_at == $created_at) and
|
|
232
|
+
(.path == $path) and
|
|
233
|
+
(.display_title == $title)' \
|
|
234
|
+
<<< "${source_run_json}" >/dev/null
|
|
235
|
+
then
|
|
236
|
+
echo "Internal review-app handoff does not match its source workflow run." >&2
|
|
237
|
+
exit 1
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
handoff_verified=false
|
|
241
|
+
for _attempt in {1..10}; do
|
|
242
|
+
source_jobs_json="$(
|
|
243
|
+
gh api --paginate --slurp --method GET \
|
|
244
|
+
"repos/${GH_REPO}/actions/runs/${source_run_id}/jobs?filter=all&per_page=100"
|
|
245
|
+
)"
|
|
246
|
+
if jq -e --arg job_name "${expected_job_name}" '
|
|
247
|
+
[
|
|
248
|
+
.[]?.jobs[]?
|
|
249
|
+
| select(.name == $job_name)
|
|
250
|
+
| .steps[]?
|
|
251
|
+
| select(.name == "Dispatch latest accepted review app intent" and .conclusion == "success")
|
|
252
|
+
]
|
|
253
|
+
| length > 0
|
|
254
|
+
' <<< "${source_jobs_json}" >/dev/null
|
|
255
|
+
then
|
|
256
|
+
handoff_verified=true
|
|
257
|
+
break
|
|
258
|
+
fi
|
|
259
|
+
sleep 1
|
|
260
|
+
done
|
|
261
|
+
if [[ "${handoff_verified}" != "true" ]]; then
|
|
262
|
+
echo "Internal review-app handoff was not issued by the expected successful redispatch step." >&2
|
|
263
|
+
exit 1
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
printf '%s\n' 'reuse=true' >> "$GITHUB_OUTPUT"
|
|
267
|
+
exit 0
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
run_created_at="$(
|
|
271
|
+
gh api --method GET "repos/${GH_REPO}/actions/runs/${RUN_ID}" --jq '.created_at'
|
|
272
|
+
)"
|
|
273
|
+
if [[ ! "${run_created_at}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
|
|
274
|
+
echo "GitHub returned an invalid workflow run creation time." >&2
|
|
275
|
+
exit 1
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
intent_json="$(
|
|
279
|
+
jq -cn \
|
|
280
|
+
--argjson pr "${PR_NUMBER}" \
|
|
281
|
+
--arg operation "${OPERATION}" \
|
|
282
|
+
--arg event "${EVENT_NAME}" \
|
|
283
|
+
--arg actor "${SOURCE_ACTOR}" \
|
|
284
|
+
--argjson run_id "${RUN_ID}" \
|
|
285
|
+
--arg created_at "${run_created_at}" \
|
|
286
|
+
'{pr: $pr, operation: $operation, event: $event, actor: $actor, run_id: $run_id, created_at: $created_at}'
|
|
287
|
+
)"
|
|
288
|
+
intent_body="<!-- cpflow-review-app-intent-v1 ${intent_json} -->"
|
|
289
|
+
printf '%s\n' "body=${intent_body}" 'record=true' >> "$GITHUB_OUTPUT"
|
|
290
|
+
|
|
291
|
+
- name: Record accepted review app intent
|
|
292
|
+
if: steps.intent.outputs.record == 'true'
|
|
293
|
+
id: record
|
|
294
|
+
shell: bash
|
|
295
|
+
env:
|
|
296
|
+
GH_TOKEN: ${{ github.token }}
|
|
297
|
+
GH_REPO: ${{ github.repository }}
|
|
298
|
+
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
|
299
|
+
INTENT_BODY: ${{ steps.intent.outputs.body }}
|
|
300
|
+
run: |
|
|
301
|
+
set -euo pipefail
|
|
302
|
+
|
|
303
|
+
if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]] ||
|
|
304
|
+
[[ ! "${INTENT_BODY}" =~ ^\<\!--\ cpflow-review-app-intent-v1\ \{.*\}\ --\>$ ]]; then
|
|
305
|
+
echo "Invalid prepared review-app intent." >&2
|
|
306
|
+
exit 1
|
|
307
|
+
fi
|
|
308
|
+
gh api --silent --method POST "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
|
|
309
|
+
-f "body=${INTENT_BODY}"
|
|
310
|
+
printf '%s\n' 'accepted=true' >> "$GITHUB_OUTPUT"
|
|
311
|
+
|
|
33
312
|
deploy:
|
|
313
|
+
needs: authorize-comment-command
|
|
34
314
|
# Skip synchronize/opened events from fork PRs at the job level — they cannot access
|
|
35
315
|
# repository secrets anyway, so running any steps just burns billable minutes. Users
|
|
36
|
-
# can still
|
|
37
|
-
#
|
|
316
|
+
# can still request a fork PR deploy via `+review-app-deploy` (gated above by
|
|
317
|
+
# repository permission) or workflow_dispatch, but the source guard below prevents it.
|
|
38
318
|
if: |
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
github.
|
|
44
|
-
|
|
45
|
-
|
|
319
|
+
needs.authorize-comment-command.outputs.allowed == 'true' &&
|
|
320
|
+
((github.event_name == 'pull_request' &&
|
|
321
|
+
github.event.pull_request.head.repo.full_name == github.repository) ||
|
|
322
|
+
github.event_name == 'workflow_dispatch' ||
|
|
323
|
+
(github.event_name == 'issue_comment' &&
|
|
324
|
+
github.event.issue.pull_request &&
|
|
325
|
+
contains(fromJson('["+review-app-deploy","+review-app-deploy\n","+review-app-deploy\r\n"]'), github.event.comment.body)))
|
|
46
326
|
runs-on: ubuntu-latest
|
|
47
327
|
timeout-minutes: 45
|
|
328
|
+
outputs:
|
|
329
|
+
image_built: ${{ steps.build-image.outcome == 'success' }}
|
|
330
|
+
concurrency:
|
|
331
|
+
group: cpflow-review-app-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
|
332
|
+
# Enter the shared deploy/delete queue only after comment authorization. A denied
|
|
333
|
+
# public comment must not displace a legitimate pending review-app operation.
|
|
334
|
+
# Let an in-flight deploy finish because cancellation can leave a partial rollout.
|
|
335
|
+
cancel-in-progress: false
|
|
48
336
|
|
|
49
337
|
steps:
|
|
338
|
+
# GitHub keeps only one pending member of a concurrency group and may replace
|
|
339
|
+
# it without FIFO ordering. Reconcile the newest authenticated accepted intent so
|
|
340
|
+
# whichever job survives applies (or redispatches) the requested final state.
|
|
341
|
+
- name: Reconcile latest accepted review app intent
|
|
342
|
+
id: intent
|
|
343
|
+
shell: bash
|
|
344
|
+
env:
|
|
345
|
+
GH_TOKEN: ${{ github.token }}
|
|
346
|
+
GH_REPO: ${{ github.repository }}
|
|
347
|
+
CURRENT_OPERATION: deploy
|
|
348
|
+
run: |
|
|
349
|
+
set -euo pipefail
|
|
350
|
+
|
|
351
|
+
if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ ]]; then
|
|
352
|
+
echo "Invalid pull request id while reconciling review-app intent." >&2
|
|
353
|
+
exit 1
|
|
354
|
+
fi
|
|
355
|
+
case "${CURRENT_OPERATION}" in
|
|
356
|
+
deploy|delete) ;;
|
|
357
|
+
*) echo "Invalid current review-app operation: ${CURRENT_OPERATION}" >&2; exit 1 ;;
|
|
358
|
+
esac
|
|
359
|
+
|
|
360
|
+
comments_json="$(
|
|
361
|
+
gh api --paginate --slurp --method GET \
|
|
362
|
+
"repos/${GH_REPO}/issues/${PR_NUMBER}/comments?per_page=100"
|
|
363
|
+
)"
|
|
364
|
+
intents_json="$(
|
|
365
|
+
jq -c --argjson pr "${PR_NUMBER}" '
|
|
366
|
+
[
|
|
367
|
+
.[][]?
|
|
368
|
+
| select(.user.login == "github-actions[bot]")
|
|
369
|
+
| .body
|
|
370
|
+
| select(type == "string")
|
|
371
|
+
| select(startswith("<!-- cpflow-review-app-intent-v1 ") and endswith(" -->"))
|
|
372
|
+
| ltrimstr("<!-- cpflow-review-app-intent-v1 ")
|
|
373
|
+
| rtrimstr(" -->")
|
|
374
|
+
| fromjson?
|
|
375
|
+
| select(.pr == $pr)
|
|
376
|
+
| select(
|
|
377
|
+
(.operation == "deploy" and
|
|
378
|
+
(.event == "issue_comment" or
|
|
379
|
+
.event == "workflow_dispatch" or
|
|
380
|
+
.event == "pull_request")) or
|
|
381
|
+
(.operation == "delete" and
|
|
382
|
+
(.event == "issue_comment" or
|
|
383
|
+
.event == "workflow_dispatch" or
|
|
384
|
+
.event == "pull_request_target"))
|
|
385
|
+
)
|
|
386
|
+
| select((.actor | type) == "string" and (.actor | length) > 0)
|
|
387
|
+
| select((.run_id | type) == "number" and .run_id > 0 and (.run_id | floor) == .run_id)
|
|
388
|
+
| select(
|
|
389
|
+
(.created_at | type) == "string" and
|
|
390
|
+
(.created_at | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"))
|
|
391
|
+
)
|
|
392
|
+
]
|
|
393
|
+
| sort_by([.created_at, .run_id])
|
|
394
|
+
| reverse
|
|
395
|
+
' <<< "${comments_json}"
|
|
396
|
+
)"
|
|
397
|
+
latest_intent="$(jq -c '.[0] // empty' <<< "${intents_json}")"
|
|
398
|
+
if [[ -z "${latest_intent}" ]]; then
|
|
399
|
+
echo "No accepted review-app intent exists for PR #${PR_NUMBER}." >&2
|
|
400
|
+
exit 1
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
latest_operation="$(jq -r '.operation' <<< "${latest_intent}")"
|
|
404
|
+
latest_event="$(jq -r '.event' <<< "${latest_intent}")"
|
|
405
|
+
latest_actor="$(jq -r '.actor' <<< "${latest_intent}")"
|
|
406
|
+
latest_run_id="$(jq -r '.run_id' <<< "${latest_intent}")"
|
|
407
|
+
latest_created_at="$(jq -r '.created_at' <<< "${latest_intent}")"
|
|
408
|
+
expected_workflow_path=".github/workflows/cpflow-${latest_operation}-review-app.yml"
|
|
409
|
+
case "${latest_operation}" in
|
|
410
|
+
deploy) expected_run_name="Deploy Review App - PR #${PR_NUMBER}" ;;
|
|
411
|
+
delete) expected_run_name="Delete Review App - PR #${PR_NUMBER}" ;;
|
|
412
|
+
esac
|
|
413
|
+
source_run_json="$(
|
|
414
|
+
gh api --method GET "repos/${GH_REPO}/actions/runs/${latest_run_id}"
|
|
415
|
+
)"
|
|
416
|
+
if ! jq -e \
|
|
417
|
+
--argjson run_id "${latest_run_id}" \
|
|
418
|
+
--arg event "${latest_event}" \
|
|
419
|
+
--arg actor "${latest_actor}" \
|
|
420
|
+
--arg created_at "${latest_created_at}" \
|
|
421
|
+
--arg path "${expected_workflow_path}" \
|
|
422
|
+
--arg title "${expected_run_name}" \
|
|
423
|
+
--arg repo "${GH_REPO}" \
|
|
424
|
+
--argjson pr "${PR_NUMBER}" \
|
|
425
|
+
'(.id == $run_id) and
|
|
426
|
+
(.event == $event) and
|
|
427
|
+
(.actor.login == $actor) and
|
|
428
|
+
(.created_at == $created_at) and
|
|
429
|
+
(.path == $path) and
|
|
430
|
+
(.display_title == $title) and
|
|
431
|
+
($event != "pull_request" or
|
|
432
|
+
(([.pull_requests[]?.number] | index($pr)) != null and
|
|
433
|
+
.head_repository.full_name == $repo))' \
|
|
434
|
+
<<< "${source_run_json}" >/dev/null
|
|
435
|
+
then
|
|
436
|
+
echo "Latest review-app intent does not match its originating workflow run." >&2
|
|
437
|
+
exit 1
|
|
438
|
+
fi
|
|
439
|
+
source_jobs_json="$(
|
|
440
|
+
gh api --paginate --slurp --method GET \
|
|
441
|
+
"repos/${GH_REPO}/actions/runs/${latest_run_id}/jobs?filter=all&per_page=100"
|
|
442
|
+
)"
|
|
443
|
+
if ! jq -e '
|
|
444
|
+
[
|
|
445
|
+
.[]?.jobs[]?
|
|
446
|
+
| select(
|
|
447
|
+
.name == "authorize-comment-command" or
|
|
448
|
+
(.name | endswith(" / authorize-comment-command"))
|
|
449
|
+
)
|
|
450
|
+
| .steps[]?
|
|
451
|
+
| select(.name == "Record accepted review app intent" and .conclusion == "success")
|
|
452
|
+
]
|
|
453
|
+
| length > 0
|
|
454
|
+
' <<< "${source_jobs_json}" >/dev/null
|
|
455
|
+
then
|
|
456
|
+
echo "Latest review-app intent was not emitted by a successful recording step." >&2
|
|
457
|
+
exit 1
|
|
458
|
+
fi
|
|
459
|
+
if [[ "${latest_event}" == "issue_comment" || "${latest_event}" == "workflow_dispatch" ]]; then
|
|
460
|
+
permission="$(
|
|
461
|
+
gh api --method GET \
|
|
462
|
+
"repos/${GH_REPO}/collaborators/${latest_actor}/permission" \
|
|
463
|
+
--jq '.permission'
|
|
464
|
+
)"
|
|
465
|
+
case "${permission}" in
|
|
466
|
+
write|maintain|admin) ;;
|
|
467
|
+
read|triage|none)
|
|
468
|
+
echo "Latest review-app intent actor no longer has permission to run it: ${permission}." >&2
|
|
469
|
+
exit 1
|
|
470
|
+
;;
|
|
471
|
+
"")
|
|
472
|
+
echo "GitHub returned an empty permission for the latest review-app intent actor." >&2
|
|
473
|
+
exit 1
|
|
474
|
+
;;
|
|
475
|
+
*)
|
|
476
|
+
echo "Unexpected repository permission for the latest review-app intent actor: ${permission}" >&2
|
|
477
|
+
exit 1
|
|
478
|
+
;;
|
|
479
|
+
esac
|
|
480
|
+
fi
|
|
481
|
+
|
|
482
|
+
if [[ "${latest_operation}" != "${CURRENT_OPERATION}" ]]; then
|
|
483
|
+
printf '%s\n' \
|
|
484
|
+
'redispatch=true' \
|
|
485
|
+
"operation=${latest_operation}" \
|
|
486
|
+
"run_id=${latest_run_id}" >> "$GITHUB_OUTPUT"
|
|
487
|
+
echo "Latest accepted ${latest_operation} intent requires a canonical redispatch."
|
|
488
|
+
exit 0
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
printf '%s\n' \
|
|
492
|
+
'redispatch=false' \
|
|
493
|
+
"operation=${latest_operation}" \
|
|
494
|
+
"run_id=${latest_run_id}" \
|
|
495
|
+
"event=${latest_event}" >> "$GITHUB_OUTPUT"
|
|
496
|
+
echo "Applying latest accepted ${latest_operation} intent from workflow run ${latest_run_id}."
|
|
497
|
+
|
|
498
|
+
- name: Dispatch latest accepted review app intent
|
|
499
|
+
id: redispatch
|
|
500
|
+
if: steps.intent.outputs.redispatch == 'true'
|
|
501
|
+
shell: bash
|
|
502
|
+
env:
|
|
503
|
+
GH_TOKEN: ${{ github.token }}
|
|
504
|
+
GH_REPO: ${{ github.repository }}
|
|
505
|
+
SOURCE_OPERATION: deploy
|
|
506
|
+
TARGET_OPERATION: ${{ steps.intent.outputs.operation }}
|
|
507
|
+
INTENT_RUN_ID: ${{ steps.intent.outputs.run_id }}
|
|
508
|
+
run: |
|
|
509
|
+
set -euo pipefail
|
|
510
|
+
|
|
511
|
+
if [[ ! "${PR_NUMBER}" =~ ^[0-9]+$ || ! "${INTENT_RUN_ID}" =~ ^[0-9]+$ ||
|
|
512
|
+
! "${GITHUB_RUN_ID}" =~ ^[0-9]+$ ]]; then
|
|
513
|
+
echo "Invalid run identity while redispatching review-app intent." >&2
|
|
514
|
+
exit 1
|
|
515
|
+
fi
|
|
516
|
+
case "${SOURCE_OPERATION}:${TARGET_OPERATION}" in
|
|
517
|
+
deploy:delete|delete:deploy) ;;
|
|
518
|
+
*) echo "Invalid review-app redispatch operation pair." >&2; exit 1 ;;
|
|
519
|
+
esac
|
|
520
|
+
|
|
521
|
+
default_branch="$(gh api --method GET "repos/${GH_REPO}" --jq '.default_branch')"
|
|
522
|
+
if [[ -z "${default_branch}" ]]; then
|
|
523
|
+
echo "GitHub returned an empty default branch while redispatching review-app intent." >&2
|
|
524
|
+
exit 1
|
|
525
|
+
fi
|
|
526
|
+
workflow_file="cpflow-${TARGET_OPERATION}-review-app.yml"
|
|
527
|
+
dispatch_json="$(
|
|
528
|
+
gh api --method POST \
|
|
529
|
+
-H "X-GitHub-Api-Version: 2026-03-10" \
|
|
530
|
+
"repos/${GH_REPO}/actions/workflows/${workflow_file}/dispatches" \
|
|
531
|
+
-f "ref=${default_branch}" \
|
|
532
|
+
-f "inputs[pr_number]=${PR_NUMBER}" \
|
|
533
|
+
-f "inputs[reconcile_intent_run_id]=${INTENT_RUN_ID}"
|
|
534
|
+
)"
|
|
535
|
+
dispatched_run_id="$(jq -r '.workflow_run_id // empty' <<< "${dispatch_json}")"
|
|
536
|
+
if [[ ! "${dispatched_run_id}" =~ ^[0-9]+$ ]]; then
|
|
537
|
+
echo "GitHub did not return a valid workflow run id for the review-app redispatch." >&2
|
|
538
|
+
exit 1
|
|
539
|
+
fi
|
|
540
|
+
source_created_at="$(
|
|
541
|
+
gh api --method GET "repos/${GH_REPO}/actions/runs/${GITHUB_RUN_ID}" --jq '.created_at'
|
|
542
|
+
)"
|
|
543
|
+
if [[ ! "${source_created_at}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; then
|
|
544
|
+
echo "GitHub returned an invalid source run creation time for the review-app redispatch." >&2
|
|
545
|
+
exit 1
|
|
546
|
+
fi
|
|
547
|
+
handoff_json="$(
|
|
548
|
+
jq -cn \
|
|
549
|
+
--argjson pr "${PR_NUMBER}" \
|
|
550
|
+
--arg operation "${TARGET_OPERATION}" \
|
|
551
|
+
--argjson intent_run_id "${INTENT_RUN_ID}" \
|
|
552
|
+
--argjson dispatch_run_id "${dispatched_run_id}" \
|
|
553
|
+
--argjson source_run_id "${GITHUB_RUN_ID}" \
|
|
554
|
+
--arg created_at "${source_created_at}" \
|
|
555
|
+
'{pr: $pr, operation: $operation, intent_run_id: $intent_run_id,
|
|
556
|
+
dispatch_run_id: $dispatch_run_id, source_run_id: $source_run_id,
|
|
557
|
+
created_at: $created_at}'
|
|
558
|
+
)"
|
|
559
|
+
handoff_body="<!-- cpflow-review-app-redispatch-v1 ${handoff_json} -->"
|
|
560
|
+
gh api --silent --method POST "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
|
|
561
|
+
-f "body=${handoff_body}"
|
|
562
|
+
echo "Redispatched ${TARGET_OPERATION} for latest accepted review-app intent run ${INTENT_RUN_ID}."
|
|
563
|
+
|
|
564
|
+
- name: Stop superseded review app operation
|
|
565
|
+
if: steps.intent.outputs.redispatch == 'true'
|
|
566
|
+
shell: bash
|
|
567
|
+
env:
|
|
568
|
+
TARGET_OPERATION: ${{ steps.intent.outputs.operation }}
|
|
569
|
+
INTENT_RUN_ID: ${{ steps.intent.outputs.run_id }}
|
|
570
|
+
run: |
|
|
571
|
+
echo "Stopped superseded operation after redispatching ${TARGET_OPERATION} intent ${INTENT_RUN_ID}." >&2
|
|
572
|
+
exit 1
|
|
573
|
+
|
|
574
|
+
# Re-read live PR state after intent reconciliation so a close-triggered
|
|
575
|
+
# delete that ran first cannot be followed by a stale deploy recreation.
|
|
576
|
+
- name: Verify pull request is still open
|
|
577
|
+
shell: bash
|
|
578
|
+
env:
|
|
579
|
+
GH_TOKEN: ${{ github.token }}
|
|
580
|
+
GH_REPO: ${{ github.repository }}
|
|
581
|
+
run: |
|
|
582
|
+
set -euo pipefail
|
|
583
|
+
|
|
584
|
+
pr_state="$(gh api --method GET "repos/${GH_REPO}/pulls/${PR_NUMBER}" --jq '.state')"
|
|
585
|
+
if [[ "${pr_state}" != "open" ]]; then
|
|
586
|
+
echo "Refusing stale review-app deploy because PR #${PR_NUMBER} is ${pr_state:-unknown}." >&2
|
|
587
|
+
exit 1
|
|
588
|
+
fi
|
|
589
|
+
|
|
50
590
|
- name: React to deploy command
|
|
51
591
|
if: github.event_name == 'issue_comment'
|
|
52
592
|
continue-on-error: true
|
|
@@ -253,23 +793,36 @@ jobs:
|
|
|
253
793
|
esac
|
|
254
794
|
|
|
255
795
|
- name: Skip auto deploy until a review app is created
|
|
256
|
-
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists != 'true' &&
|
|
796
|
+
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists != 'true' && steps.intent.outputs.event == 'pull_request'
|
|
257
797
|
shell: bash
|
|
258
798
|
run: |
|
|
259
799
|
{
|
|
800
|
+
echo "## Docker image not built"
|
|
801
|
+
echo
|
|
802
|
+
echo "This successful review-app check did not build or deploy an image."
|
|
803
|
+
echo "Outcome: \`image_built=false\`."
|
|
804
|
+
echo
|
|
260
805
|
echo "Review app ${APP_NAME} does not exist yet."
|
|
261
806
|
echo "Create it with +review-app-deploy as the PR comment body."
|
|
262
807
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
263
808
|
|
|
264
809
|
- name: Setup review app if it does not exist yet
|
|
265
810
|
id: setup-review-app
|
|
266
|
-
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists != 'true' &&
|
|
811
|
+
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists != 'true' && steps.intent.outputs.event != 'pull_request'
|
|
267
812
|
working-directory: app
|
|
268
813
|
shell: bash
|
|
269
814
|
run: |
|
|
270
815
|
set -euo pipefail
|
|
271
816
|
cpflow setup-app -a "${APP_NAME}" --org "${CPLN_ORG}"
|
|
272
817
|
|
|
818
|
+
- name: Refresh existing review app templates
|
|
819
|
+
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && steps.check-app.outputs.exists == 'true'
|
|
820
|
+
working-directory: app
|
|
821
|
+
shell: bash
|
|
822
|
+
run: |
|
|
823
|
+
set -euo pipefail
|
|
824
|
+
cpflow setup-app -a "${APP_NAME}" --org "${CPLN_ORG}" --refresh-templates
|
|
825
|
+
|
|
273
826
|
- name: Create initial PR comment
|
|
274
827
|
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
|
|
275
828
|
id: create-comment
|
|
@@ -313,6 +866,8 @@ jobs:
|
|
|
313
866
|
repo: context.repo.repo,
|
|
314
867
|
ref: process.env.PR_SHA,
|
|
315
868
|
environment: `review/${process.env.APP_NAME}`,
|
|
869
|
+
transient_environment: true,
|
|
870
|
+
production_environment: false,
|
|
316
871
|
auto_merge: false,
|
|
317
872
|
required_contexts: [], // intentional: review apps deploy regardless of required status checks
|
|
318
873
|
description: `Control Plane review app for PR #${process.env.PR_NUMBER}`
|
|
@@ -356,6 +911,7 @@ jobs:
|
|
|
356
911
|
});
|
|
357
912
|
|
|
358
913
|
- name: Build Docker image
|
|
914
|
+
id: build-image
|
|
359
915
|
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
|
|
360
916
|
uses: ./.cpflow/.github/actions/cpflow-build-docker-image
|
|
361
917
|
with:
|
|
@@ -439,16 +995,95 @@ jobs:
|
|
|
439
995
|
|
|
440
996
|
cpflow deploy-image "${deploy_args[@]}"
|
|
441
997
|
|
|
442
|
-
- name:
|
|
998
|
+
- name: Wait for deployment health
|
|
443
999
|
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
|
|
1000
|
+
id: health-check
|
|
1001
|
+
uses: ./.cpflow/.github/actions/cpflow-wait-for-health
|
|
1002
|
+
with:
|
|
1003
|
+
workload_name: ${{ env.PRIMARY_WORKLOAD || 'rails' }}
|
|
1004
|
+
app_name: ${{ steps.review-config.outputs.app_name }}
|
|
1005
|
+
org: ${{ steps.review-config.outputs.cpln_org }}
|
|
1006
|
+
# Review apps use their own health knobs so teams can keep production
|
|
1007
|
+
# promotion stricter or slower without changing PR feedback behavior.
|
|
1008
|
+
max_retries: ${{ vars.REVIEW_APP_HEALTH_CHECK_RETRIES || '24' }}
|
|
1009
|
+
interval_seconds: ${{ vars.REVIEW_APP_HEALTH_CHECK_INTERVAL || '15' }}
|
|
1010
|
+
accepted_statuses: ${{ vars.REVIEW_APP_HEALTH_CHECK_ACCEPTED_STATUSES || '200 301 302' }}
|
|
1011
|
+
curl_max_time: ${{ vars.REVIEW_APP_HEALTH_CHECK_CURL_MAX_TIME || '10' }}
|
|
1012
|
+
|
|
1013
|
+
- name: Retrieve app URL
|
|
1014
|
+
if: steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success') && steps.health-check.outcome == 'success'
|
|
444
1015
|
id: workload
|
|
445
1016
|
working-directory: app
|
|
446
1017
|
shell: bash
|
|
1018
|
+
env:
|
|
1019
|
+
VERIFIED_WORKLOAD_ENDPOINT: ${{ steps.health-check.outputs.endpoint }}
|
|
447
1020
|
run: |
|
|
448
1021
|
set -euo pipefail
|
|
449
1022
|
workload_name="${PRIMARY_WORKLOAD:-rails}"
|
|
450
1023
|
workload_url="$(cpln workload get "${workload_name}" --gvc "${APP_NAME}" --org "${CPLN_ORG}" -o json | jq -r '.status.endpoint // empty')"
|
|
451
|
-
|
|
1024
|
+
app_url="${VERIFIED_WORKLOAD_ENDPOINT:-${workload_url}}"
|
|
1025
|
+
|
|
1026
|
+
if [[ -n "${workload_url}" ]]; then
|
|
1027
|
+
gvc_stderr="$(mktemp)"
|
|
1028
|
+
if ! gvc_json="$(cpln gvc get "${APP_NAME}" --org "${CPLN_ORG}" -o json 2>"${gvc_stderr}")"; then
|
|
1029
|
+
echo "::warning::Could not read GVC app_domain; falling back to verified workload endpoint."
|
|
1030
|
+
sed 's/^/cpln gvc get: /' "${gvc_stderr}" >&2
|
|
1031
|
+
gvc_json=""
|
|
1032
|
+
fi
|
|
1033
|
+
rm -f "${gvc_stderr}"
|
|
1034
|
+
|
|
1035
|
+
app_domain_template="$(
|
|
1036
|
+
jq -r '.spec.env[]? | select(.name == "app_domain") | .value // empty' <<< "${gvc_json}" | head -n 1
|
|
1037
|
+
)"
|
|
1038
|
+
|
|
1039
|
+
if [[ -n "${app_domain_template}" ]]; then
|
|
1040
|
+
derived_app_url="$(
|
|
1041
|
+
ruby -ruri -e '
|
|
1042
|
+
workload_name, workload_url, app_domain_template, app_name = ARGV
|
|
1043
|
+
begin
|
|
1044
|
+
uri = URI(workload_url)
|
|
1045
|
+
rescue URI::InvalidURIError
|
|
1046
|
+
exit
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
host_prefix = "#{workload_name}-"
|
|
1050
|
+
host_suffix = ".cpln.app"
|
|
1051
|
+
|
|
1052
|
+
unless uri.host&.start_with?(host_prefix) && uri.host&.end_with?(host_suffix)
|
|
1053
|
+
exit
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
raw_cpln_gvc_alias = uri.host.delete_prefix(host_prefix).delete_suffix(host_suffix)
|
|
1057
|
+
cpln_gvc_alias = raw_cpln_gvc_alias.split(".").first
|
|
1058
|
+
if cpln_gvc_alias.to_s.empty?
|
|
1059
|
+
exit
|
|
1060
|
+
end
|
|
1061
|
+
# Avoid a literal command-substitution token here because actionlint flags it inside single-quoted Ruby.
|
|
1062
|
+
cpln_gvc_alias_token = "$" + "(CPLN_GVC_ALIAS)"
|
|
1063
|
+
app_url = app_domain_template
|
|
1064
|
+
.gsub(cpln_gvc_alias_token, cpln_gvc_alias)
|
|
1065
|
+
.gsub("{{APP_NAME}}", app_name)
|
|
1066
|
+
|
|
1067
|
+
begin
|
|
1068
|
+
app_uri = URI(app_url)
|
|
1069
|
+
if app_uri.path.to_s.empty? && app_uri.query.nil? && app_uri.fragment.nil?
|
|
1070
|
+
app_url = "#{app_url}/"
|
|
1071
|
+
end
|
|
1072
|
+
rescue URI::InvalidURIError
|
|
1073
|
+
exit
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
puts app_url
|
|
1077
|
+
' "${workload_name}" "${workload_url}" "${app_domain_template}" "${APP_NAME}"
|
|
1078
|
+
)"
|
|
1079
|
+
app_url="${derived_app_url:-${app_url}}"
|
|
1080
|
+
fi
|
|
1081
|
+
fi
|
|
1082
|
+
|
|
1083
|
+
{
|
|
1084
|
+
echo "workload_url=${workload_url}"
|
|
1085
|
+
echo "app_url=${app_url}"
|
|
1086
|
+
} >> "$GITHUB_OUTPUT"
|
|
452
1087
|
|
|
453
1088
|
- name: Finalize deployment status
|
|
454
1089
|
if: always() && steps.config.outputs.ready == 'true' && steps.source.outputs.allowed == 'true' && (steps.check-app.outputs.exists == 'true' || steps.setup-review-app.outcome == 'success')
|
|
@@ -456,7 +1091,7 @@ jobs:
|
|
|
456
1091
|
env:
|
|
457
1092
|
COMMENT_ID: ${{ steps.create-comment.outputs.comment-id }}
|
|
458
1093
|
DEPLOYMENT_ID: ${{ steps.init-deployment.outputs.result }}
|
|
459
|
-
APP_URL: ${{ steps.workload.outputs.
|
|
1094
|
+
APP_URL: ${{ steps.workload.outputs.app_url }}
|
|
460
1095
|
JOB_STATUS: ${{ job.status }}
|
|
461
1096
|
with:
|
|
462
1097
|
script: |
|