Skip to content
253 changes: 253 additions & 0 deletions .github/workflows/_submodule_check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: "Submodule Fast-Forward Check"

on:
workflow_call:
inputs:
base_ref:
required: true
type: string
description: "Target branch to check against"
head_ref:
required: true
type: string
description: "Feature branch name"
pr_number:
required: true
type: string
description: "Pull request number"
head_sha:
required: true
type: string
description: "Head commit SHA of the feature branch"

jobs:
check:
name: Check submodule fast-forward
runs-on: ubuntu-latest
outputs:
failed: ${{ steps.check.outputs.failed }}
changed: ${{ steps.check.outputs.changed }}
comment_body: ${{ steps.check.outputs.comment_body }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: 'recursive'

- name: Fetch target branch reference
run: |
git fetch origin ${{ inputs.base_ref }}

- name: Check submodule fast-forward status
id: check
shell: bash -x -e {0}
run: |
echo "Checking submodules are fast-forwarded..."

# Get current submodule status
echo "Current submodule status:"
git submodule status

failed=0
changed=0
success_body=""
failed_body=""

# Process each submodule from git submodule status
while read -r line; do
# Extract commit and path from: " <commit> <path> (<branch_info>)"
current_commit=$(echo "$line" | awk '{print $1}' | sed 's/^[+-]//')
submodule_path=$(echo "$line" | awk '{print $2}')

if [[ -z "$current_commit" ]] || [[ -z "$submodule_path" ]]; then
continue
fi

submodule_name=$(basename "$submodule_path")
echo ""
echo "Checking $submodule_name at $submodule_path"
echo "Current commit: $current_commit"

# Get target branch commit for this submodule
target_commit=$(git ls-tree origin/${{ inputs.base_ref }} "$submodule_path" | awk '{print $3}')

if [[ -z "$target_commit" ]]; then
echo "❌ Could not find $submodule_name in ${{ inputs.base_ref }} branch"
failed=1
continue
fi

echo "Target commit: $target_commit"

# Analyze the relationship between target and current commits
cd "$submodule_path"

# Check if this is a shallow repository and unshallow if needed
if git rev-parse --is-shallow-repository >/dev/null 2>&1 && [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
echo "📦 $submodule_name: Detected shallow clone, fetching full history..."
git fetch --unshallow >/dev/null 2>&1 || {
echo "⚠️ Warning: Failed to unshallow repository. Ancestry checks may be limited."
}
fi

# Get GitHub repository URL for comment
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
if [[ "$remote_url" == *.git ]]; then
github_repo="${remote_url%.git}"
else
github_repo="$remote_url"
fi

# Case 1: Same commit
if [[ "$current_commit" = "$target_commit" ]]; then
echo "✅ $submodule_name: PR branch matches ${{ inputs.base_ref }} branch (same commit)"
# No change, so don't add to changed count or comment

# Case 2: Check if target commit is an ancestor of current commit (current is fast-forward)
elif git merge-base --is-ancestor "$target_commit" "$current_commit" 2>/dev/null; then
echo "✅ $submodule_name: PR branch is ahead of ${{ inputs.base_ref }} branch (fast-forward)"
echo "📊 Commits added in PR #${{ inputs.pr_number }} (${{ inputs.head_ref }} branch):"
git log --oneline --graph "$target_commit".."$current_commit" 2>/dev/null || echo " (Unable to show progression - possibly shallow clone)"
changed=1
success_body+="$submodule_name: ✅ PR branch is ahead of ${{ inputs.base_ref }} branch (fast-forward)"$'\n'

# Case 3: Check if current commit is an ancestor of target commit (current is behind)
elif git merge-base --is-ancestor "$current_commit" "$target_commit" 2>/dev/null; then
echo "❌ $submodule_name: PR branch is BEHIND ${{ inputs.base_ref }} branch"
echo " Submodule needs to be updated to include recent changes from ${{ inputs.base_ref }}"
echo "📊 Missing commits from ${{ inputs.base_ref }} that should be included:"
git log --oneline --graph "$current_commit".."$target_commit" 2>/dev/null || echo " (Unable to show missing commits)"
failed=1
changed=1
if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
failed_body+="$submodule_name: ❌ PR branch is BEHIND ${{ inputs.base_ref }} branch"$'\n'
failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
fi

else
# Case 4: Commits have diverged or have no common ancestor
common_ancestor=$(git merge-base "$target_commit" "$current_commit" 2>/dev/null)

if [ -n "$common_ancestor" ]; then
echo "❌ $submodule_name: Commits have DIVERGED from a common ancestor"
echo " This indicates parallel development - manual merge may be required"
echo ""
echo "📊 Divergence analysis:"
echo " Common ancestor: $common_ancestor"
git log --oneline -1 "$common_ancestor" 2>/dev/null || echo " (Unable to show common ancestor)"
echo ""
echo " For detailed commit history inspection:"
failed=1
changed=1
if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
echo " TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"
echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"
failed_body+="$submodule_name: ❌ Commits have DIVERGED from a common ancestor"$'\n'
failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
else
echo " Repository: $github_repo (unable to generate GitHub URLs)"
echo " TARGET (${{ inputs.base_ref }} branch): $target_commit"
echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"
failed_body+="$submodule_name: ❌ Commits have DIVERGED from a common ancestor"$'\n'
failed_body+=" TARGET (${{ inputs.base_ref }} branch): $target_commit"$'\n'
failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"$'\n\n'
fi
else
echo "❌ $submodule_name: Commits have NO COMMON ANCESTOR"
echo " This indicates commits are from completely different repositories or history"
echo ""
echo "📊 For detailed commit inspection:"
failed=1
changed=1
if [[ -n "$github_repo" && "$github_repo" == https://github.com/* ]]; then
echo " TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"
echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"
failed_body+="$submodule_name: ❌ Commits have NO COMMON ANCESTOR"$'\n'
failed_body+=" TARGET (${{ inputs.base_ref }} branch): $github_repo/commits/$target_commit/"$'\n'
failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $github_repo/commits/$current_commit/"$'\n\n'
else
echo " Repository: $github_repo (unable to generate GitHub URLs)"
echo " TARGET (${{ inputs.base_ref }} branch): $target_commit"
echo " CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"
failed_body+="$submodule_name: ❌ Commits have NO COMMON ANCESTOR"$'\n'
failed_body+=" TARGET (${{ inputs.base_ref }} branch): $target_commit"$'\n'
failed_body+=" CURRENT (PR #${{ inputs.pr_number }} from ${{ inputs.head_ref }}): $current_commit"$'\n\n'
fi
fi
fi
cd "$GITHUB_WORKSPACE"

done < <(git submodule status)

# Set outputs
echo "failed=$failed" >> $GITHUB_OUTPUT
echo "changed=$changed" >> $GITHUB_OUTPUT
if [[ $changed -eq 1 ]]; then
comment_body=""
if [[ -n "$success_body" ]]; then
comment_body+="### ✅ Submodules that are properly updated:"$'\n'
comment_body+="$success_body"$'\n'
fi
if [[ -n "$failed_body" ]]; then
comment_body+="### ❌ Submodules that need attention:"$'\n'
comment_body+="$failed_body"
fi
echo "comment_body<<EOF" >> $GITHUB_OUTPUT
echo "$comment_body" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
fi

if [[ $failed -eq 1 ]]; then
echo ""
echo "❌ One or more submodules are not fast-forwarded"
echo "Please ensure submodule commits are fast-forwards of the ${{ inputs.base_ref }} branch"
exit 1
fi

echo ""
echo "✅ All submodules are properly fast-forwarded"

comment:
name: Comment on PR
needs: [check]
runs-on: ubuntu-latest
if: always() && needs.check.outputs.changed == '1'
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const failed = '${{ needs.check.outputs.failed }}' === '1';
const title = failed ?
'## ❌ Submodule Fast-Forward Check Failed' :
'## ✅ Submodule Fast-Forward Check Results';

const commentBody = `${title}

**Check based on commit:** ${{ inputs.head_sha }} (PR #${{ inputs.pr_number }} from \`${{ inputs.head_ref }}\`)

${{ needs.check.outputs.comment_body }}
${failed ? 'Please ensure all submodule commits are fast-forwards of the ${{ inputs.base_ref }} branch before merging.' : 'All submodule changes look good! ✨'}`;

await github.rest.issues.createComment({
issue_number: ${{ inputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
11 changes: 11 additions & 0 deletions .github/workflows/cicd-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ jobs:

echo "test_level=$TEST_LEVEL" | tee -a "$GITHUB_OUTPUT"

submodule-check:
name: Check submodule fast-forward
needs: [pre-flight]
if: github.event_name == 'pull_request'
uses: ./.github/workflows/_submodule_check.yml
with:
base_ref: ${{ github.base_ref }}
head_ref: ${{ github.head_ref }}
pr_number: ${{ github.event.number }}
head_sha: ${{ github.event.pull_request.head.sha }}

lint-check:
name: Lint check
needs: [pre-flight]
Expand Down
Loading