1+ name : Verify CAP Checksum & Structure
2+
3+ on :
4+ pull_request :
5+ types : [opened, reopened, synchronize, edited, ready_for_review]
6+ paths :
7+ - ' **/*.zip'
8+
9+ jobs :
10+ verify-zips :
11+ runs-on : ubuntu-latest
12+ env :
13+ BASE_SHA : ${{ github.event.pull_request.base.sha }}
14+ HEAD_SHA : ${{ github.event.pull_request.head.sha }}
15+
16+ steps :
17+ - name : Checkout PR HEAD
18+ uses : actions/checkout@v4
19+ with :
20+ fetch-depth : 0
21+
22+ - name : Step 1 - Verify sha256 in manifest.json for changed ZIPs
23+ shell : bash
24+ run : |
25+ set -euo pipefail
26+
27+ # Collect changed ZIPs into a file that the next step can reuse
28+ : > changed_zips.txt
29+
30+ while IFS= read -r -d '' f; do
31+ [[ "$f" == *.zip ]] && printf '%s\n' "$f" >> changed_zips.txt
32+ done < <(git diff --name-only -z "$BASE_SHA" "$HEAD_SHA")
33+
34+ if [[ ! -s changed_zips.txt ]]; then
35+ echo "No .zip files changed in this PR. Nothing to verify."
36+ exit 0
37+ fi
38+
39+ echo "Changed ZIP files:"
40+ sed 's/^/ - /' changed_zips.txt
41+
42+ while IFS= read -r zip_path; do
43+ # If deleted in PR head, skip
44+ if [[ ! -f "$zip_path" ]]; then
45+ echo "Skipping (not present in PR head): $zip_path"
46+ continue
47+ fi
48+
49+ dir="$(dirname "$zip_path")"
50+ manifest_path="$dir/manifest.json"
51+
52+ if [[ ! -f "$manifest_path" ]]; then
53+ echo "::error file=$manifest_path::manifest.json not found next to ZIP ($zip_path)"
54+ exit 1
55+ fi
56+
57+ # Compute checksum of the ZIP
58+ computed="$(sha256sum "$zip_path" | awk '{print $1}' | tr '[:upper:]' '[:lower:]')"
59+
60+ # Read sha256 from manifest.json
61+ manifest_sha="$(jq -r '.sha256 // empty' "$manifest_path" | tr '[:upper:]' '[:lower:]')"
62+
63+ if [[ -z "$manifest_sha" || "$manifest_sha" == "null" ]]; then
64+ echo "::error file=$manifest_path::Missing or empty \"sha256\" field in manifest.json"
65+ exit 1
66+ fi
67+
68+ echo "ZIP: $zip_path"
69+ echo "Computed: $computed"
70+ echo "Manifest: $manifest_sha"
71+
72+ if [[ "$computed" != "$manifest_sha" ]]; then
73+ echo "::error file=$manifest_path::sha256 mismatch for $zip_path (computed=$computed, manifest=$manifest_sha)"
74+ exit 1
75+ fi
76+ done < changed_zips.txt
77+
78+ - name : Step 2 - Verify manifest zip matches file + manifest version is unique
79+ shell : bash
80+ run : |
81+ set -euo pipefail
82+
83+ [[ -s changed_zips.txt ]] || exit 0
84+
85+ while IFS= read -r zip_path; do
86+ [[ -f "$zip_path" ]] || continue
87+
88+ dir="$(dirname "$zip_path")"
89+ manifest_path="$dir/manifest.json"
90+ catalog_path="$dir/catalog.json"
91+
92+ if [[ ! -f "$catalog_path" ]]; then
93+ echo "::error file=$catalog_path::catalog.json not found next to ZIP ($zip_path)"
94+ exit 1
95+ fi
96+
97+ if [[ ! -f "$manifest_path" ]]; then
98+ echo "::error file=$manifest_path::manifest.json not found next to ZIP ($zip_path)"
99+ exit 1
100+ fi
101+
102+ zip_file="$(basename "$zip_path")"
103+ manifest_zip="$(jq -r '.zip // empty' "$manifest_path")"
104+ if [[ -z "$manifest_zip" || "$manifest_zip" == "null" ]]; then
105+ echo "::error file=$manifest_path::Missing \"zip\" field in manifest.json"
106+ exit 1
107+ fi
108+
109+ version="$(jq -r '.version // empty' "$manifest_path")"
110+ if [[ -z "$version" || "$version" == "null" ]]; then
111+ echo "::error file=$manifest_path::Missing \"version\" field in manifest.json"
112+ exit 1
113+ fi
114+
115+ # Verify zip field in manifest matches file name
116+ if [[ "$manifest_zip" != "$zip_file" ]]; then
117+ echo "::error file=$manifest_path::manifest.json \"zip\" ($manifest_zip) does not match changed zip filename ($zip_file)"
118+ exit 1
119+ fi
120+
121+ # Verify new version in manifest is unique
122+ if jq -e --arg v "$version" '.versions[]? | select(.version == $v)' "$catalog_path" >/dev/null; then
123+ echo "::error file=$catalog_path::Version $version already exists in catalog.json"
124+ exit 1
125+ fi
126+ done < changed_zips.txt
127+
128+ - name : Step 3 - Unzip and verify top level folder structure
129+ shell : bash
130+ run : |
131+ set -euo pipefail
132+
133+ # List of allowed top level folder names
134+ allowed=("impex" "app-configuration" "storefront-next" "cartridges")
135+
136+ [[ -s changed_zips.txt ]] || exit 0
137+
138+ while IFS= read -r zip_path; do
139+ [[ -f "$zip_path" ]] || continue
140+
141+ # Unzip file to temp dir
142+ tmpdir="$(mktemp -d)"
143+ unzip -q "$zip_path" -d "$tmpdir"
144+
145+ # Root should be exactly one directory (the wrapper folder)
146+ mapfile -t roots < <(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | grep -v '^__MACOSX$' | sort -u)
147+ if [[ ${#roots[@]} -ne 1 ]]; then
148+ echo "::error file=$zip_path::Expected exactly 1 root directory after unzip, found ${#roots[@]}: ${roots[*]}"
149+ rm -rf "$tmpdir"
150+ exit 1
151+ fi
152+ root="$tmpdir/${roots[0]}"
153+
154+ # Check immediate child directories of root are allowed
155+ mapfile -t children < <(find "$root" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | grep -v '^__MACOSX$' | sort -u)
156+ for c in "${children[@]}"; do
157+ ok=false
158+ for a in "${allowed[@]}"; do
159+ [[ "$c" == "$a" ]] && ok=true && break
160+ done
161+ if [[ "$ok" == "false" ]]; then
162+ echo "::error file=$zip_path::Disallowed directory under root: \"$c\". Allowed: ${allowed[*]}"
163+ rm -rf "$tmpdir"
164+ exit 1
165+ fi
166+ done
167+ rm -rf "$tmpdir"
168+ done < changed_zips.txt
169+
170+ - name : Step 4 - [Tax] Verify hooks exist and scripts resolve
171+ shell : bash
172+ run : |
173+ set -euo pipefail
174+
175+ required_hooks=(
176+ "dw.apps.checkout.tax.calculate"
177+ "dw.apps.checkout.tax.commit"
178+ "dw.apps.checkout.tax.cancel"
179+ )
180+
181+ [[ -s changed_zips.txt ]] || exit 0
182+
183+ is_tax_app=false
184+
185+ while IFS= read -r zip_path; do
186+ [[ -f "$zip_path" ]] || continue
187+
188+ # Only run for ZIPs under tax/ at repo root
189+ case "$zip_path" in
190+ tax/*) is_tax_app=true ;;
191+ *) continue ;;
192+ esac
193+
194+ tmpdir="$(mktemp -d)"
195+ trap 'rm -rf "$tmpdir"' RETURN
196+
197+ unzip -q "$zip_path" -d "$tmpdir"
198+
199+ # Find cartridges/site_cartridges anywhere under the unzip root
200+ base="$(find "$tmpdir" -type d -path '*/cartridges/site_cartridges' -print -quit)"
201+ if [[ -z "$base" || ! -d "$base" ]]; then
202+ echo "::error file=$zip_path::Missing directory cartridges/site_cartridges in ZIP"
203+ exit 1
204+ fi
205+
206+ hooks_file="$(find "$base" -type f -name hooks.json -print -quit)"
207+ if [[ -z "$hooks_file" ]]; then
208+ echo "::error file=$zip_path::hooks.json not found under cartridges/site_cartridges"
209+ exit 1
210+ fi
211+
212+ hooks_dir="$(dirname "$hooks_file")"
213+ echo "ZIP: $zip_path"
214+ echo "hooks.json: $hooks_file"
215+
216+ # Ensure there are no non-required hook names
217+ while IFS= read -r name; do
218+ ok=false
219+ for hook in "${required_hooks[@]}"; do
220+ [[ "$name" == "$hook" ]] && ok=true && break
221+ done
222+ if [[ "$ok" == "false" ]]; then
223+ echo "::error file=$hooks_file::Disallowed hook \"$name\". Only allowed: ${required_hooks[*]}"
224+ exit 1
225+ fi
226+ done < <(jq -r '.hooks[]?.name // empty' "$hooks_file")
227+
228+ # Validate required hooks are present, and each has a script that exists
229+ for hook in "${required_hooks[@]}"; do
230+ script="$(jq -r --arg name "$hook" '.hooks[]? | select(.name == $name) | .script // empty' "$hooks_file" | head -n 1)"
231+
232+ if [[ -z "$script" || "$script" == "null" ]]; then
233+ echo "::error file=$hooks_file::Missing hook or script for \"$hook\""
234+ exit 1
235+ fi
236+
237+ # script is relative to hooks.json location (strip leading ./)
238+ rel="${script#./}"
239+ target="$hooks_dir/$rel"
240+
241+ if [[ ! -f "$target" ]]; then
242+ echo "::error file=$hooks_file::Script for \"$hook\" points to missing file: $script (resolved: $target)"
243+ exit 1
244+ fi
245+ done
246+
247+ echo "SUCCESS - required tax hooks and scripts verified for $zip_path"
248+
249+ rm -rf "$tmpdir"
250+ trap - RETURN
251+ done < changed_zips.txt
252+
253+ if [[ "$is_tax_app" == "false" ]]; then
254+ echo "No changed tax apps. Skipping Step 3."
255+ fi
0 commit comments