-
Notifications
You must be signed in to change notification settings - Fork 0
266 lines (246 loc) · 11.4 KB
/
Copy pathyaml-diff.yaml
File metadata and controls
266 lines (246 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
name: Compare YAML Source
# Posts a semantic diff (via dyff) of YAML source files changed in a PR as a
# sticky PR comment. Key-order changes are ignored, so reviewers see only
# value-level changes. Companion to helm-render-diff.yaml (which diffs
# rendered Helm output rather than source).
#
# Opt-out: include `/no_diffs_printing` on its own line in the PR body or as
# a comment. The same command suppresses helm-render-diff.yaml.
on:
workflow_call:
inputs:
dyff_version:
type: string
default: "1.7.1"
description: "dyff release version to install."
paths:
type: string
default: "*.yaml *.yml"
description: "Space-separated git pathspecs selecting which files to diff (passed after `git diff -- <paths>`)."
exclude_paths:
type: string
default: "**/*.enc.yaml .github/** .pre-commit-config.yaml"
description: >-
Space-separated glob patterns to exclude. Patterns ending in `/**` match a directory
prefix; patterns of the form `**/<glob>` match `<glob>` against the basename at any
depth; patterns without `/` match by basename; otherwise exact path match.
SOPS-encrypted files must remain excluded.
permissions: {}
concurrency:
group: yaml-diff-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
# Checks for the `/no_diffs_printing` opt-out. When found, the diff job is
# skipped. Shared semantics with helm-render-diff.yaml — one command, one UX.
check-cmp-state:
runs-on: ubuntu-24.04
permissions:
pull-requests: read
if: github.event_name == 'pull_request'
steps:
- name: Find suspend comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
continue-on-error: true
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
body-regex: '^\s*/no_diffs_printing' # on its own line, not as `<!-- /no_diffs_printing -->`
- name: Find suspend comment in PR body
id: pr_body
run: |
if jq -r .pull_request.body "${GITHUB_EVENT_PATH}" | grep -qE '^\s*/no_diffs_printing'; then
echo "Found /no_diffs_printing command in PR body"
echo "suspend_diffs_printing_from_pr_body=true" >> $GITHUB_OUTPUT
else
echo "Did not find /no_diffs_printing command in PR body"
echo "suspend_diffs_printing_from_pr_body=false" >> $GITHUB_OUTPUT
fi
outputs:
suspend_comment_id: ${{ steps.fc.outputs.comment-id }}
suspend_diffs_printing_from_pr_body: ${{ steps.pr_body.outputs.suspend_diffs_printing_from_pr_body }}
cmp-yaml-source:
needs: check-cmp-state
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write
if: github.event_name == 'pull_request' && needs.check-cmp-state.outputs.suspend_comment_id == 0 && needs.check-cmp-state.outputs.suspend_diffs_printing_from_pr_body == 'false'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 0
- name: install dyff
uses: giantswarm/install-binary-action@5bef88f65012037dd836117c8d344b21bb559854 # v4.1.0
with:
binary: dyff
download_url: "https://github.com/homeport/dyff/releases/download/v${version}/dyff_${version}_linux_amd64.tar.gz"
smoke_test: "${binary} version"
tarball_binary_path: "${binary}"
version: ${{ inputs.dyff_version }}
- run: which dyff
- name: compute diffs
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
PATHS: ${{ inputs.paths }}
EXCLUDE_PATHS: ${{ inputs.exclude_paths }}
run: |
set -uo pipefail
# GitHub PR comments are capped at 65536 chars. Leave headroom for wrapper markdown.
per_file_cap=5000
total_cap=60000
# Ensure base ref is available locally (fetch-depth: 0 already pulls everything,
# but be defensive about shallow clones).
git fetch --no-tags origin "${BASE_REF}" >/dev/null 2>&1 || true
base_sha=$(git rev-parse "origin/${BASE_REF}")
echo "Base: ${BASE_REF} (${base_sha})"
# Split inputs into arrays without shell-expanding the globs.
read -r -a path_globs <<< "${PATHS}"
read -r -a exclude_globs <<< "${EXCLUDE_PATHS}"
# Exclude matcher. Handles these pattern shapes:
# foo/** — directory prefix
# **/*.something — basename glob at any depth
# *.something — basename match (no slash in pattern)
# anything else — exact path match
should_exclude() {
local path="$1"
local g prefix base_glob
for g in "${exclude_globs[@]}"; do
if [[ "${g}" == */"**" ]]; then
prefix="${g%/**}"
[[ "${path}" == "${prefix}/"* ]] && return 0
[[ "${path}" == "${prefix}" ]] && return 0
elif [[ "${g}" == "**/"* && "${g#**/}" != *"/"* ]]; then
# `**/<glob>` — match <glob> against the basename at any depth (e.g. `**/*.enc.yaml`)
base_glob="${g#**/}"
# shellcheck disable=SC2053
[[ "$(basename "${path}")" == ${base_glob} ]] && return 0
elif [[ "${g}" != *"/"* ]]; then
# shellcheck disable=SC2053
[[ "$(basename "${path}")" == ${g} ]] && return 0
else
[[ "${path}" == "${g}" ]] && return 0
fi
done
return 1
}
# List changed files: added + modified only. Renames are not detected (no -M),
# so renamed-without-content-change shows as delete+add and the add gets diffed.
# Deletes are filtered out — diffing a removed file's old content adds noise.
mapfile -t changed < <(git diff --name-only --diff-filter=AM "${base_sha}...HEAD" -- "${path_globs[@]}" | sort -u)
mkdir -p /tmp/yaml-diff
: > /tmp/yaml-diff/body
found_differences=
total_size=0
truncated=
for path in "${changed[@]}"; do
[[ -z "${path}" ]] && continue
if should_exclude "${path}"; then
echo "Excluded: ${path}"
continue
fi
status=$(git diff --name-status --diff-filter=AM "${base_sha}...HEAD" -- "${path}" | awk '{print $1}' | head -n1)
: > /tmp/yaml-diff/file-diff
case "${status}" in
A)
# New file: there's no base content to diff against, so we just note it.
# dyff against /dev/null can crash on some YAML shapes; the line-diff in
# GitHub's UI already shows the added file content.
echo "(Added — see file diff in PR for content)" >> /tmp/yaml-diff/file-diff
;;
M)
tmpold=$(mktemp)
git show "${base_sha}:${path}" > "${tmpold}" 2>/dev/null
if dyff between --set-exit-code --ignore-order-changes --omit-header \
--use-go-patch-style "${tmpold}" "${path}" > /tmp/yaml-diff/file-diff 2>&1; then
# exit 0 = no semantic difference (e.g. only key reordering); skip this file
rm -f "${tmpold}"
continue
else
res=$?
if [[ ${res} -eq 255 ]]; then
echo "Diff error" >> /tmp/yaml-diff/file-diff
fi
fi
rm -f "${tmpold}"
;;
*)
echo "(unhandled status: ${status})" >> /tmp/yaml-diff/file-diff
;;
esac
section=$(mktemp)
{
echo
echo "@@ ${path} @@"
cat /tmp/yaml-diff/file-diff
} > "${section}"
section_size=$(wc -c < "${section}")
if (( section_size > per_file_cap )); then
head -c "${per_file_cap}" "${section}" > "${section}.trunc"
printf '\n... (truncated, file diff exceeded %d bytes)\n' "${per_file_cap}" >> "${section}.trunc"
mv "${section}.trunc" "${section}"
section_size=$(wc -c < "${section}")
fi
if (( total_size + section_size > total_cap )); then
truncated=1
rm -f "${section}"
break
fi
cat "${section}" >> /tmp/yaml-diff/body
total_size=$(( total_size + section_size ))
found_differences=1
rm -f "${section}"
done
{
if [[ -z "${found_differences}" ]]; then
echo "<!-- YAML source diff output -->"
echo "**No semantic YAML differences** in changed source files. (Key reordering without value changes is ignored.)"
else
echo "<!-- YAML source diff output -->"
echo "**Semantic YAML source diff** — key reordering without value changes is ignored."
echo
if [[ -n "${truncated}" ]]; then
echo "⚠️ Output truncated to fit GitHub's comment size limit. See the [workflow run](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) for full output."
echo
fi
echo "<details>"
echo "<summary>Output</summary>"
echo "<!-- mandatory empty line -->"
echo
echo '```diff'
# GitHub colours a `diff` fence by line prefix: `-` red, `+` green,
# `@@ … @@` header. dyff go-patch prefixes changed values with an
# *indented* `-`/`+`; move the marker to column 0 while keeping the
# original indent *after* it, so the highlighter fires and nesting is
# still visible. The rewrite only reorders leading whitespace, so byte
# counts (and the truncation maths above) are unchanged. dyff colour
# stays off (auto-off in CI) — comments cannot render ANSI.
sed -E 's/^([[:space:]]*)([+-])([[:space:]].*)$/\2\1\3/' /tmp/yaml-diff/body
echo '```'
echo "</details>"
echo "<!-- mandatory empty line -->"
fi
echo
echo "_Suppress with \`/no_diffs_printing\` on its own line in the PR body or as a comment._"
} > /tmp/yaml-diff/comment-body
- name: Find diff comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
continue-on-error: true
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: 'YAML source diff output'
- name: Delete old comment
uses: winterjung/comment@fda92dbcb5e7e79cccd55ecb107a8a3d7802a469 # v1.1.0
continue-on-error: true
if: steps.fc.outputs.comment-id != 0
with:
type: delete
comment_id: ${{ steps.fc.outputs.comment-id }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Create comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ github.event.pull_request.number }}
body-path: /tmp/yaml-diff/comment-body