Skip to content

Commit 4a26885

Browse files
committed
feat(drupal): add PostSync PR comment Job with deployment details
- Extract NOTES.txt body into shared helper drupal.deployment-notes so NOTES.txt and the PR comment render identical content - Add github-pr-comment.yaml PostSync Job + ConfigMap that upserts a GitHub PR comment with full deployment details (URLs, basic auth, SSH, rsync commands) using GitHub App JWT auth - Job is best-effort (exits 0 on failure), lightweight (alpine:3.20, 64Mi), and only renders when github.prComment.prNumber is set - Add github.prComment values section and JSON schema entry
1 parent dace146 commit 4a26885

5 files changed

Lines changed: 273 additions & 83 deletions

File tree

drupal/templates/NOTES.txt

Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1 @@
1-
{{ $protocol := .Values.ingress.default.tls | ternary "https" "http" -}}
2-
Your site is available at:
3-
4-
{{ $protocol }}://{{- template "drupal.domain" . }}
5-
{{- range $index, $prefix := .Values.domainPrefixes }}
6-
{{- $params := dict "prefix" $prefix }}
7-
{{ $protocol}}://{{ template "drupal.domain" (merge $params $ ) }}
8-
{{- end }}
9-
{{- range $index, $domain := .Values.exposeDomains }}
10-
{{- if $domain.ssl }}
11-
{{- if $domain.ssl.enabled }}
12-
https://{{ $domain.hostname }}
13-
{{- end }}
14-
{{- else }}
15-
http://{{ $domain.hostname }}
16-
{{- end }}
17-
{{- end }}
18-
19-
{{- if .Values.mailhog.enabled }}
20-
21-
Mailhog available at:
22-
23-
http://{{- template "drupal.domain" . }}/mailhog
24-
{{- range $index, $domain := .Values.exposeDomains }}
25-
http://{{ $domain.hostname }}/mailhog
26-
{{- end }}
27-
⚠️ **DEPRECATED** mailhog is deprecated and will be removed in the future, use mailpit instead
28-
See: https://wunderio.github.io/silta/docs/silta-examples#sending-e-mail
29-
{{- end }}
30-
31-
{{- if .Values.mailpit.enabled }}
32-
33-
Mailpit available at:
34-
35-
http://{{- template "drupal.domain" . }}/mailpit
36-
{{- range $index, $domain := .Values.exposeDomains }}
37-
http://{{ $domain.hostname }}/mailpit
38-
{{- end }}
39-
{{- end }}
40-
41-
{{- if .Values.nginx.basicauth.enabled }}
42-
43-
Basic access authentication credentials:
44-
45-
Username: {{ .Values.nginx.basicauth.credentials.username }}
46-
Password: {{ .Values.nginx.basicauth.credentials.password }}
47-
{{- end }}
48-
49-
{{- if .Values.shell.enabled }}
50-
51-
SSH connection (limited access through VPN):
52-
53-
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }}
54-
55-
Downloading data from server
56-
57-
Downloading database:
58-
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }} "drush sql-dump" > {{ .Release.Namespace }}-{{ .Release.Name }}.sql
59-
60-
{{ range $index, $mount := .Values.mounts -}}
61-
{{ if eq $mount.enabled true -}}
62-
{{/* Ensure that the mount path is suffixed with a slash to download contents of the mount not the folder itself. */}}
63-
{{- $mountPath := ternary $mount.mountPath (printf "%s/" $mount.mountPath) (hasSuffix "/" $mount.mountPath) -}}
64-
Downloading files from {{ $index }}:
65-
rsync -azv -e 'ssh -A -J {{ include "drupal.jumphost" $ }}' {{ include "drupal.shellHost" $ }}:{{ $mountPath }} {{ $.Release.Namespace }}-mounts/{{ $index }}
66-
{{ end }}
67-
{{ end -}}
68-
69-
Downloading any file or folder:
70-
rsync -chavzP -e "ssh -A -J {{ include "drupal.jumphost" . }}" {{ include "drupal.shellHost" . }}:/app/remote-filename ./
71-
72-
Importing data into server (use this with caution!)
73-
74-
Importing database:
75-
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }} "drush sql-cli" < {{ .Release.Namespace }}-{{ .Release.Name }}.sql
76-
77-
{{ range $index, $mount := .Values.mounts -}}
78-
{{ if eq $mount.enabled true -}}
79-
Uploading files to {{ $index }}:
80-
rsync -azv --temp-dir=/tmp/ -e 'ssh -A -J {{ include "drupal.jumphost" $ }}' {{ $.Release.Namespace }}-mounts/{{ $index }}/ {{ include "drupal.shellHost" $ }}:{{ $mount.mountPath }}
81-
{{ end }}
82-
{{ end -}}
83-
{{- end -}}
1+
{{ include "drupal.deployment-notes" . }}

drupal/templates/_helpers.tpl

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,3 +849,99 @@ autoscaling/v2beta1
849849
{{- .Release.Name }}-sa
850850
{{- end }}
851851
{{- end }}
852+
853+
{{/*
854+
Deployment notes — shared between NOTES.txt and the PR comment Job.
855+
Outputs Markdown-formatted environment details.
856+
*/}}
857+
{{- define "drupal.deployment-notes" -}}
858+
{{ $protocol := .Values.ingress.default.tls | ternary "https" "http" -}}
859+
**Your site is available at:**
860+
861+
{{ $protocol }}://{{- template "drupal.domain" . }}
862+
{{- range $index, $prefix := .Values.domainPrefixes }}
863+
{{- $params := dict "prefix" $prefix }}
864+
{{ $protocol}}://{{ template "drupal.domain" (merge $params $ ) }}
865+
{{- end }}
866+
{{- range $index, $domain := .Values.exposeDomains }}
867+
{{- if $domain.ssl }}
868+
{{- if $domain.ssl.enabled }}
869+
https://{{ $domain.hostname }}
870+
{{- end }}
871+
{{- else }}
872+
http://{{ $domain.hostname }}
873+
{{- end }}
874+
{{- end }}
875+
{{- if .Values.mailhog.enabled }}
876+
877+
**Mailhog available at:**
878+
879+
http://{{- template "drupal.domain" . }}/mailhog
880+
{{- range $index, $domain := .Values.exposeDomains }}
881+
http://{{ $domain.hostname }}/mailhog
882+
{{- end }}
883+
> ⚠️ mailhog is deprecated — use mailpit instead.
884+
> See: https://wunderio.github.io/silta/docs/silta-examples#sending-e-mail
885+
{{- end }}
886+
{{- if .Values.mailpit.enabled }}
887+
888+
**Mailpit available at:**
889+
890+
http://{{- template "drupal.domain" . }}/mailpit
891+
{{- range $index, $domain := .Values.exposeDomains }}
892+
http://{{ $domain.hostname }}/mailpit
893+
{{- end }}
894+
{{- end }}
895+
{{- if .Values.nginx.basicauth.enabled }}
896+
897+
**Basic Auth:**
898+
899+
| | |
900+
|---|---|
901+
| Username | `{{ .Values.nginx.basicauth.credentials.username }}` |
902+
| Password | `{{ .Values.nginx.basicauth.credentials.password }}` |
903+
{{- end }}
904+
{{- if .Values.shell.enabled }}
905+
906+
**SSH connection** (limited access through VPN):
907+
908+
```
909+
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }}
910+
```
911+
912+
<details>
913+
<summary>Data transfer commands</summary>
914+
915+
**Downloading database:**
916+
```
917+
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }} "drush sql-dump" > {{ .Release.Namespace }}-{{ .Release.Name }}.sql
918+
```
919+
{{ range $index, $mount := .Values.mounts -}}
920+
{{ if eq $mount.enabled true -}}
921+
{{- $mountPath := ternary $mount.mountPath (printf "%s/" $mount.mountPath) (hasSuffix "/" $mount.mountPath) -}}
922+
**Downloading files from {{ $index }}:**
923+
```
924+
rsync -azv -e 'ssh -A -J {{ include "drupal.jumphost" $ }}' {{ include "drupal.shellHost" $ }}:{{ $mountPath }} {{ $.Release.Namespace }}-mounts/{{ $index }}
925+
```
926+
{{ end }}
927+
{{ end -}}
928+
**Downloading any file or folder:**
929+
```
930+
rsync -chavzP -e "ssh -A -J {{ include "drupal.jumphost" . }}" {{ include "drupal.shellHost" . }}:/app/remote-filename ./
931+
```
932+
933+
**Importing database** (use with caution!):
934+
```
935+
ssh {{ include "drupal.shellHost" . }} -J {{ include "drupal.jumphost" . }} "drush sql-cli" < {{ .Release.Namespace }}-{{ .Release.Name }}.sql
936+
```
937+
{{ range $index, $mount := .Values.mounts -}}
938+
{{ if eq $mount.enabled true -}}
939+
**Uploading files to {{ $index }}:**
940+
```
941+
rsync -azv --temp-dir=/tmp/ -e 'ssh -A -J {{ include "drupal.jumphost" $ }}' {{ $.Release.Namespace }}-mounts/{{ $index }}/ {{ include "drupal.shellHost" $ }}:{{ $mount.mountPath }}
942+
```
943+
{{ end }}
944+
{{ end -}}
945+
</details>
946+
{{- end -}}
947+
{{- end -}}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
{{- if .Values.github.prComment.prNumber }}
2+
---
3+
# PostSync Job that posts deployment details as a GitHub PR comment.
4+
# Only rendered for PR environments (prNumber is set by the platform chart).
5+
# Best-effort: the script exits 0 even if the GitHub API call fails,
6+
# so a comment failure does not mark the sync as failed.
7+
apiVersion: batch/v1
8+
kind: Job
9+
metadata:
10+
name: "{{ .Release.Name }}-pr-comment"
11+
labels:
12+
{{- include "drupal.release_labels" . | nindent 4 }}
13+
annotations:
14+
argocd.argoproj.io/hook: PostSync
15+
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
16+
spec:
17+
activeDeadlineSeconds: 120
18+
backoffLimit: 0
19+
ttlSecondsAfterFinished: 300
20+
template:
21+
metadata:
22+
labels:
23+
{{- include "drupal.release_labels" . | nindent 8 }}
24+
spec:
25+
restartPolicy: Never
26+
enableServiceLinks: false
27+
containers:
28+
- name: pr-comment
29+
image: alpine:3.20
30+
command: ["/bin/sh", "-c"]
31+
args:
32+
- |
33+
set -e
34+
apk add --no-cache curl openssl jq >/dev/null 2>&1
35+
36+
# --- GitHub App authentication ---
37+
PRIVATE_KEY_FILE="/github-app/githubAppPrivateKey"
38+
39+
# Build JWT (RS256, valid 5 minutes)
40+
NOW=$(date +%s)
41+
IAT=$((NOW - 60))
42+
EXP=$((NOW + 300))
43+
HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
44+
PAYLOAD=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$IAT" "$EXP" "$GITHUB_APP_ID" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
45+
SIGNATURE=$(printf '%s.%s' "$HEADER" "$PAYLOAD" | openssl dgst -sha256 -sign "$PRIVATE_KEY_FILE" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
46+
JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}"
47+
48+
# Exchange JWT for installation access token
49+
TOKEN=$(curl -sf -X POST \
50+
-H "Authorization: Bearer ${JWT}" \
51+
-H "Accept: application/vnd.github+json" \
52+
"https://api.github.com/app/installations/${GITHUB_APP_INSTALLATION_ID}/access_tokens" \
53+
| jq -r '.token') || {
54+
echo "WARNING: Failed to get GitHub installation token. Skipping PR comment."
55+
exit 0
56+
}
57+
58+
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
59+
echo "WARNING: GitHub token is empty. Skipping PR comment."
60+
exit 0
61+
fi
62+
63+
# --- Post or update PR comment ---
64+
REPO="{{ .Values.github.prComment.repository }}"
65+
PR_NUMBER="{{ .Values.github.prComment.prNumber }}"
66+
COMMENT_TAG="{{ .Values.github.prComment.commentTag }}-{{ .Release.Name }}"
67+
MARKER="<!-- ${COMMENT_TAG} -->"
68+
69+
# Find existing comment by marker
70+
EXISTING_ID=$(curl -sf \
71+
-H "Authorization: token ${TOKEN}" \
72+
-H "Accept: application/vnd.github+json" \
73+
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \
74+
| jq -r ".[] | select(.body | contains(\"${MARKER}\")) | .id" \
75+
| head -1) || true
76+
77+
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
78+
curl -sf -X PATCH \
79+
-H "Authorization: token ${TOKEN}" \
80+
-H "Accept: application/vnd.github+json" \
81+
-d "$(jq -n --rawfile body /comment/comment-body.txt '{body: $body}')" \
82+
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments/${EXISTING_ID}" \
83+
>/dev/null && echo "Updated existing PR comment." || echo "WARNING: Failed to update PR comment."
84+
else
85+
curl -sf -X POST \
86+
-H "Authorization: token ${TOKEN}" \
87+
-H "Accept: application/vnd.github+json" \
88+
-d "$(jq -n --rawfile body /comment/comment-body.txt '{body: $body}')" \
89+
"https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments" \
90+
>/dev/null && echo "Posted new PR comment." || echo "WARNING: Failed to post PR comment."
91+
fi
92+
exit 0
93+
env:
94+
- name: GITHUB_APP_ID
95+
valueFrom:
96+
secretKeyRef:
97+
name: {{ .Values.github.prComment.secretName }}
98+
key: githubAppID
99+
- name: GITHUB_APP_INSTALLATION_ID
100+
valueFrom:
101+
secretKeyRef:
102+
name: {{ .Values.github.prComment.secretName }}
103+
key: githubAppInstallationID
104+
volumeMounts:
105+
- name: github-app-key
106+
mountPath: /github-app
107+
readOnly: true
108+
- name: comment-body
109+
mountPath: /comment
110+
readOnly: true
111+
resources:
112+
requests:
113+
cpu: 50m
114+
memory: 32Mi
115+
limits:
116+
memory: 64Mi
117+
volumes:
118+
- name: github-app-key
119+
secret:
120+
secretName: {{ .Values.github.prComment.secretName }}
121+
items:
122+
- key: githubAppPrivateKey
123+
path: githubAppPrivateKey
124+
- name: comment-body
125+
configMap:
126+
name: {{ .Release.Name }}-pr-comment-body
127+
---
128+
# ConfigMap holding the Helm-rendered comment body.
129+
# Separating it from the Job args avoids shell escaping issues with
130+
# the rendered Markdown content.
131+
apiVersion: v1
132+
kind: ConfigMap
133+
metadata:
134+
name: {{ .Release.Name }}-pr-comment-body
135+
labels:
136+
{{- include "drupal.release_labels" . | nindent 4 }}
137+
annotations:
138+
argocd.argoproj.io/hook: PostSync
139+
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
140+
data:
141+
comment-body.txt: |
142+
### :rocket: Deployment — `{{ .Release.Name }}`
143+
{{ include "drupal.deployment-notes" . | nindent 4 }}
144+
<!-- {{ .Values.github.prComment.commentTag }}-{{ .Release.Name }} -->
145+
{{- end }}

drupal/values.schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
"clusterDomain": { "type": "string" },
66
"projectName": { "type": "string" },
77
"environmentName": { "type": "string" },
8+
"github": {
9+
"type": "object",
10+
"properties": {
11+
"prComment": {
12+
"type": "object",
13+
"properties": {
14+
"prNumber": { "type": "string" },
15+
"repository": { "type": "string" },
16+
"secretName": { "type": "string" },
17+
"commentTag": { "type": "string" }
18+
}
19+
}
20+
}
21+
},
822
"branchName": { "type": "string" },
923
"cleanup": {
1024
"type": "object",

drupal/values.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ projectName: ""
2020
# This name is mainly used to create nice subdomains for each environment.
2121
environmentName: ""
2222

23+
# --- GitHub PR Comment
24+
# When prNumber is set (injected by the platform chart for PR environments),
25+
# a PostSync Job posts deployment details as a PR comment via the GitHub API.
26+
# Requires the argocd-github-app secret in the namespace (synced by Kyverno).
27+
github:
28+
prComment:
29+
# Set automatically by the platform chart for PR environments.
30+
prNumber: ""
31+
# GitHub repository owner/name, e.g. "wunderio/client-fi-mysite".
32+
# Set automatically by the platform chart from tenant.repoURL.
33+
repository: ""
34+
# Name of the Kubernetes secret containing GitHub App credentials.
35+
# Must have keys: githubAppID, githubAppInstallationID, githubAppPrivateKey.
36+
secretName: argocd-github-app
37+
# Marker string used to upsert (update-or-create) the comment.
38+
commentTag: "silta-deploy"
39+
2340
# Configure image pull secrets for the containers. This is not needed on GKE.
2441
# See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
2542
imagePullSecrets: []

0 commit comments

Comments
 (0)