1+ name : GUIDs in PR are unique
2+
3+ permissions :
4+ pull-requests : write
5+ contents : read
6+
7+ on :
8+ workflow_call :
9+ secrets :
10+ AccessToken :
11+ required : true
12+
13+ jobs :
14+ check-duplicate-guids :
15+ if : github.repository_owner == 'MicrosoftDocs'
16+ runs-on : ubuntu-latest
17+ steps :
18+ - name : Checkout
19+ uses : actions/checkout@v4
20+ with :
21+ fetch-depth : 0
22+ token : ${{ secrets.AccessToken }}
23+
24+ - name : Extract GUIDs from PR files
25+ id : extract
26+ run : |
27+ LC_ALL=C
28+ export LC_ALL
29+
30+ # Get markdown files added or modified in this PR
31+ CHANGED_FILES=$(git diff --name-only --diff-filter=AM origin/${{ github.base_ref }}...HEAD -- '*.md' '**/*.md' | grep -v '/includes/' || true)
32+
33+ if [ -z "$CHANGED_FILES" ]; then
34+ echo "No markdown files changed in this PR."
35+ echo "has_guids=false" >> "$GITHUB_OUTPUT"
36+ exit 0
37+ fi
38+
39+ echo "Changed files:"
40+ echo "$CHANGED_FILES"
41+ echo ""
42+
43+ # Extract GUIDs from changed files into a pattern file
44+ PATTERN_FILE=$(mktemp)
45+ echo "pattern_file=$PATTERN_FILE" >> "$GITHUB_OUTPUT"
46+
47+ while IFS= read -r file; do
48+ [ -f "$file" ] || continue
49+ GUID=$(sed '1s/^\xEF\xBB\xBF//' "$file" | tr -d '\r' | sed -n '2,/^---$/p' | grep -i '^awa-articleGuid:' | sed 's/^[^:]*:\s*//' | tr -d '[:space:]')
50+ if [ -n "$GUID" ]; then
51+ echo "$GUID" >> "$PATTERN_FILE"
52+ echo "Found GUID $GUID in $file"
53+ else
54+ echo "No GUID in $file (will be assigned on merge)"
55+ fi
56+ done <<< "$CHANGED_FILES"
57+
58+ if [ ! -s "$PATTERN_FILE" ]; then
59+ echo ""
60+ echo "No GUIDs to check."
61+ echo "has_guids=false" >> "$GITHUB_OUTPUT"
62+ exit 0
63+ fi
64+
65+ echo "has_guids=true" >> "$GITHUB_OUTPUT"
66+
67+ - name : Check for duplicates across repo
68+ id : check
69+ if : steps.extract.outputs.has_guids == 'true'
70+ run : |
71+ LC_ALL=C
72+ export LC_ALL
73+
74+ PATTERN_FILE="${{ steps.extract.outputs.pattern_file }}"
75+
76+ DUPES=$(find . -name '*.md' -not -path '*/includes/*' -print0 | \
77+ xargs -0 awk '
78+ BEGIN {
79+ while ((getline g < "'"$PATTERN_FILE"'") > 0) {
80+ gsub(/[[:space:]]/, "", g)
81+ if (length(g) > 0) guids[tolower(g)] = 1
82+ }
83+ }
84+ FNR > 50 { nextfile }
85+ tolower($0) ~ /awa-articleguid:/ {
86+ val = $0
87+ sub(/^[^:]*:[[:space:]]*/, "", val)
88+ gsub(/[[:space:]]/, "", val)
89+ val = tolower(val)
90+ if (val in guids) {
91+ count[val]++
92+ files[val] = files[val] FILENAME "|"
93+ }
94+ nextfile
95+ }
96+ END {
97+ for (g in count) {
98+ if (count[g] > 1) {
99+ print g "\t" files[g]
100+ }
101+ }
102+ }
103+ ' || true)
104+
105+ rm "$PATTERN_FILE"
106+
107+ if [ -n "$DUPES" ]; then
108+ echo "$DUPES" > /tmp/dupes.txt
109+ echo "has_dupes=true" >> "$GITHUB_OUTPUT"
110+ echo "::error::Duplicate awa-articleGuid values found in this PR!"
111+ exit 1
112+ fi
113+
114+ echo "All GUIDs in this PR are unique across the repository."
115+
116+ - name : Comment on PR
117+ if : always() && steps.check.outputs.has_dupes == 'true'
118+ uses : actions/github-script@v7
119+ with :
120+ github-token : ${{ secrets.AccessToken }}
121+ script : |
122+ const fs = require('fs');
123+ const crypto = require('crypto');
124+ const { execSync } = require('child_process');
125+
126+ const dupes = fs.readFileSync('/tmp/dupes.txt', 'utf8').trim();
127+ const baseRef = process.env.GITHUB_BASE_REF || 'main';
128+
129+ // Check if a file had this GUID on the base branch
130+ function hadGuidOnBase(file, guid) {
131+ try {
132+ const content = execSync(`git show "origin/${baseRef}:${file}" 2>/dev/null`, { encoding: 'utf8' });
133+ const match = content.match(/awa-articleGuid:\s*(\S+)/i);
134+ return match && match[1].toLowerCase() === guid.toLowerCase();
135+ } catch {
136+ return false;
137+ }
138+ }
139+
140+ let body = '## One or more duplicate GUIDs were found\n\n';
141+ body += 'Update the `awa-articleGuid` field in each file in the **Action required** table with the provided GUID.\n\n';
142+
143+ for (const line of dupes.split('\n')) {
144+ const [guid, fileStr] = line.split('\t');
145+ const allFiles = fileStr.split('|').filter(Boolean).map(f => f.replace(/^\.\//, ''));
146+
147+ const keepFiles = allFiles.filter(f => hadGuidOnBase(f, guid));
148+ const changeFiles = allFiles.filter(f => !hadGuidOnBase(f, guid));
149+
150+ // If all files are new to this GUID, keep the first one and change the rest
151+ if (keepFiles.length === 0 && changeFiles.length > 1) {
152+ keepFiles.push(changeFiles.shift());
153+ }
154+
155+ body += `**Duplicate GUID:** \`${guid}\`\n\n`;
156+
157+ if (changeFiles.length > 0) {
158+ body += 'Action required:\n';
159+ body += '| File path | Update to | New GUID |\n| --- | :---: | --- |\n';
160+ for (const f of changeFiles) {
161+ const newGuid = crypto.randomUUID();
162+ body += `| \`${f}\` | \u2192 | \`${newGuid}\` |\n`;
163+ }
164+ body += '\n';
165+ }
166+
167+ if (keepFiles.length > 0) {
168+ body += 'Do not change:\n';
169+ body += '| File path | Current GUID |\n| --- | --- |\n';
170+ for (const f of keepFiles) {
171+ body += `| \`${f}\` | \`${guid}\` |\n`;
172+ }
173+ body += '\n';
174+ }
175+ }
176+
177+ // Delete previous duplicate GUID comments
178+ const comments = await github.rest.issues.listComments({
179+ owner: context.repo.owner,
180+ repo: context.repo.repo,
181+ issue_number: context.issue.number
182+ });
183+
184+ for (const c of comments.data) {
185+ if (c.body.startsWith('## One or more duplicate GUIDs were found')) {
186+ await github.rest.issues.deleteComment({
187+ owner: context.repo.owner,
188+ repo: context.repo.repo,
189+ comment_id: c.id
190+ });
191+ }
192+ }
193+
194+ await github.rest.issues.createComment({
195+ owner: context.repo.owner,
196+ repo: context.repo.repo,
197+ issue_number: context.issue.number,
198+ body: body
199+ });
0 commit comments