1+ # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License");
4+ # you may not use this file except in compliance with the License.
5+ # You may obtain a copy of the License at
6+ #
7+ # http://www.apache.org/licenses/LICENSE-2.0
8+ #
9+ # Unless required by applicable law or agreed to in writing, software
10+ # distributed under the License is distributed on an "AS IS" BASIS,
11+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ # See the License for the specific language governing permissions and
13+ # limitations under the License.
14+
15+ name : " Submodule Fast-Forward Check"
16+
17+ on :
18+ workflow_call :
19+ inputs :
20+ base_ref :
21+ required : true
22+ type : string
23+ description : " Target branch to check against"
24+ head_ref :
25+ required : true
26+ type : string
27+ description : " Feature branch name"
28+ pr_number :
29+ required : true
30+ type : string
31+ description : " Pull request number"
32+ head_sha :
33+ required : true
34+ type : string
35+ description : " Head commit SHA of the feature branch"
36+
37+ jobs :
38+ check :
39+ name : Check submodule fast-forward
40+ runs-on : ubuntu-latest
41+ outputs :
42+ failed : ${{ steps.check.outputs.failed }}
43+ changed : ${{ steps.check.outputs.changed }}
44+ comment_body : ${{ steps.check.outputs.comment_body }}
45+ steps :
46+ - name : Checkout repository
47+ uses : actions/checkout@v4
48+ with :
49+ submodules : ' recursive'
50+
51+ - name : Fetch target branch reference
52+ run : |
53+ git fetch origin ${{ inputs.base_ref }}
54+
55+ - name : Check submodule fast-forward status
56+ id : check
57+ shell : bash -x -e {0}
58+ run : |
59+ echo "Checking submodules are fast-forwarded..."
60+
61+ # Get current submodule status
62+ echo "Current submodule status:"
63+ git submodule status
64+
65+ failed=0
66+ changed=0
67+ success_body=""
68+ failed_body=""
69+
70+ # Process each submodule from git submodule status
71+ while read -r line; do
72+ # Extract commit and path from: " <commit> <path> (<branch_info>)"
73+ current_commit=$(echo "$line" | awk '{print $1}' | sed 's/^[+-]//')
74+ submodule_path=$(echo "$line" | awk '{print $2}')
75+
76+ if [[ -z "$current_commit" ]] || [[ -z "$submodule_path" ]]; then
77+ continue
78+ fi
79+
80+ submodule_name=$(basename "$submodule_path")
81+ echo ""
82+ echo "Checking $submodule_name at $submodule_path"
83+ echo "Current commit: $current_commit"
84+
85+ # Get target branch commit for this submodule
86+ target_commit=$(git ls-tree origin/${{ inputs.base_ref }} "$submodule_path" | awk '{print $3}')
87+
88+ if [[ -z "$target_commit" ]]; then
89+ echo "❌ Could not find $submodule_name in ${{ inputs.base_ref }} branch"
90+ failed=1
91+ continue
92+ fi
93+
94+ echo "Target commit: $target_commit"
95+
96+ # Analyze the relationship between target and current commits
97+ cd "$submodule_path"
98+
99+ # Check if this is a shallow repository and unshallow if needed
100+ if git rev-parse --is-shallow-repository >/dev/null 2>&1 && [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
101+ echo "📦 $submodule_name: Detected shallow clone, fetching full history..."
102+ git fetch --unshallow >/dev/null 2>&1 || {
103+ echo "⚠️ Warning: Failed to unshallow repository. Ancestry checks may be limited."
104+ }
105+ fi
106+
107+ # Get GitHub repository URL for comment
108+ remote_url=$(git remote get-url origin 2>/dev/null || echo "")
109+ if [[ "$remote_url" == *.git ]]; then
110+ github_repo="${remote_url%.git}"
111+ else
112+ github_repo="$remote_url"
113+ fi
114+
115+ # Case 1: Same commit
116+ if [[ "$current_commit" = "$target_commit" ]]; then
117+ echo "✅ $submodule_name: PR branch matches ${{ inputs.base_ref }} branch (same commit)"
118+ # No change, so don't add to changed count or comment
119+
120+ # Case 2: Check if target commit is an ancestor of current commit (current is fast-forward)
121+ elif git merge-base --is-ancestor "$target_commit" "$current_commit" 2>/dev/null; then
122+ echo "✅ $submodule_name: PR branch is ahead of ${{ inputs.base_ref }} branch (fast-forward)"
123+ echo "📊 Commits added in PR #${{ inputs.pr_number }} (${{ inputs.head_ref }} branch):"
124+ git log --oneline --graph "$target_commit".."$current_commit" 2>/dev/null || echo " (Unable to show progression - possibly shallow clone)"
125+ changed=1
126+ success_body+="$submodule_name: ✅ PR branch is ahead of ${{ inputs.base_ref }} branch (fast-forward)"$'\n'
127+
128+ # Case 3: Check if current commit is an ancestor of target commit (current is behind)
129+ elif git merge-base --is-ancestor "$current_commit" "$target_commit" 2>/dev/null; then
130+ echo "❌ $submodule_name: PR branch is BEHIND ${{ inputs.base_ref }} branch"
131+ echo " Submodule needs to be updated to include recent changes from ${{ inputs.base_ref }}"
132+ echo "📊 Missing commits from ${{ inputs.base_ref }} that should be included:"
133+ git log --oneline --graph "$current_commit".."$target_commit" 2>/dev/null || echo " (Unable to show missing commits)"
134+ failed=1
135+ changed=1
136+ if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
137+ failed_body+="$submodule_name: ❌ PR branch is BEHIND ${{ inputs.base_ref }} branch"$'\n'
138+ failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
139+ failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
140+ fi
141+
142+ else
143+ # Case 4: Commits have diverged or have no common ancestor
144+ common_ancestor=$(git merge-base "$target_commit" "$current_commit" 2>/dev/null)
145+
146+ if [ -n "$common_ancestor" ]; then
147+ echo "❌ $submodule_name: Commits have DIVERGED from a common ancestor"
148+ echo " This indicates parallel development - manual merge may be required"
149+ echo ""
150+ echo "📊 Divergence analysis:"
151+ echo " Common ancestor: $common_ancestor"
152+ git log --oneline -1 "$common_ancestor" 2>/dev/null || echo " (Unable to show common ancestor)"
153+ echo ""
154+ echo " For detailed commit history inspection:"
155+ failed=1
156+ changed=1
157+ if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
158+ echo " TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"
159+ echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"
160+ failed_body+="$submodule_name: ❌ Commits have DIVERGED from a common ancestor"$'\n'
161+ failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
162+ failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
163+ else
164+ echo " Repository: $github_repo (unable to generate GitHub URLs)"
165+ echo " TARGET (${{ inputs.base_ref }} branch): $target_commit"
166+ echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"
167+ failed_body+="$submodule_name: ❌ Commits have DIVERGED from a common ancestor"$'\n'
168+ failed_body+=" TARGET (${{ inputs.base_ref }} branch): $target_commit"$'\n'
169+ failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"$'\n\n'
170+ fi
171+ else
172+ echo "❌ $submodule_name: Commits have NO COMMON ANCESTOR"
173+ echo " This indicates commits are from completely different repositories or history"
174+ echo ""
175+ echo "📊 For detailed commit inspection:"
176+ failed=1
177+ changed=1
178+ if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
179+ echo " TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"
180+ echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"
181+ failed_body+="$submodule_name: ❌ Commits have NO COMMON ANCESTOR"$'\n'
182+ failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
183+ failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
184+ else
185+ echo " Repository: $github_repo (unable to generate GitHub URLs)"
186+ echo " TARGET (${{ inputs.base_ref }} branch): $target_commit"
187+ echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"
188+ failed_body+="$submodule_name: ❌ Commits have NO COMMON ANCESTOR"$'\n'
189+ failed_body+=" TARGET (${{ inputs.base_ref }} branch): $target_commit"$'\n'
190+ failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"$'\n\n'
191+ fi
192+ fi
193+ fi
194+ cd "$GITHUB_WORKSPACE"
195+
196+ done < <(git submodule status)
197+
198+ # Set outputs
199+ echo "failed=$failed" >> $GITHUB_OUTPUT
200+ echo "changed=$changed" >> $GITHUB_OUTPUT
201+ if [[ $changed -eq 1 ]]; then
202+ comment_body=""
203+ if [[ -n "$success_body" ]]; then
204+ comment_body+="### ✅ Submodules that are properly updated:"$'\n'
205+ comment_body+="$success_body"$'\n'
206+ fi
207+ if [[ -n "$failed_body" ]]; then
208+ comment_body+="### ❌ Submodules that need attention:"$'\n'
209+ comment_body+="$failed_body"
210+ fi
211+ echo "comment_body<<EOF" >> $GITHUB_OUTPUT
212+ echo "$comment_body" >> $GITHUB_OUTPUT
213+ echo "EOF" >> $GITHUB_OUTPUT
214+ fi
215+
216+ if [[ $failed -eq 1 ]]; then
217+ echo ""
218+ echo "❌ One or more submodules are not fast-forwarded"
219+ echo "Please ensure submodule commits are fast-forwards of the ${{ inputs.base_ref }} branch"
220+ exit 1
221+ fi
222+
223+ echo ""
224+ echo "✅ All submodules are properly fast-forwarded"
225+
226+ comment :
227+ name : Comment on PR
228+ needs : [check]
229+ runs-on : ubuntu-latest
230+ if : always() && needs.check.outputs.changed == '1'
231+ steps :
232+ - name : Comment on PR
233+ uses : actions/github-script@v7
234+ with :
235+ script : |
236+ const failed = '${{ needs.check.outputs.failed }}' === '1';
237+ const title = failed ?
238+ '## ❌ Submodule Fast-Forward Check Failed' :
239+ '## ✅ Submodule Fast-Forward Check Results';
240+
241+ const commentBody = `${title}
242+
243+ **Check based on commit:** ${{ inputs.head_sha }} (PR #${{ inputs.pr_number }} from \`${{ inputs.head_ref }}\`)
244+
245+ ${{ needs.check.outputs.comment_body }}
246+ ${failed ? 'Please ensure all submodule commits are fast-forwards of the ${{ inputs.base_ref }} branch before merging.' : 'All submodule changes look good! ✨'}`;
247+
248+ await github.rest.issues.createComment({
249+ issue_number: ${{ inputs.pr_number }},
250+ owner: context.repo.owner,
251+ repo: context.repo.repo,
252+ body: commentBody
253+ });
0 commit comments