Skip to content

ci: verify submodule pins are reachable from tracked branch#11063

Open
jkiviluoto-nv wants to merge 13 commits into
shader-slang:masterfrom
jkiviluoto-nv:submodule-pin-ci-check
Open

ci: verify submodule pins are reachable from tracked branch#11063
jkiviluoto-nv wants to merge 13 commits into
shader-slang:masterfrom
jkiviluoto-nv:submodule-pin-ci-check

Conversation

@jkiviluoto-nv

@jkiviluoto-nv jkiviluoto-nv commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a CI check that fails when any submodule pointer in .gitmodules points at a commit that is not reachable from the upstream submodule's tracked branch (the branch = override if present, otherwise the remote's default branch).

Fixes #9336.

Why

PRs only show the submodule pointer diff, so reviewers cannot tell when a pin moves to a developer branch instead of mainline. The slang-rhi haaggarwal/SER incident (#9335) is the motivating example. This check enforces the invariant automatically so the same class of mistake cannot land again.

How it works

  • extras/check-submodule-commits.sh resolves each pin via git ls-tree HEAD (no submodule checkout needed). For each submodule, it fetches into a temporary bare repo with progressive depth (50 → 500 → full) and uses git merge-base --is-ancestor to verify reachability.
  • .github/workflows/check-submodules.yml runs on pull_request and merge_group. The job always runs (no top-level paths: filter — those interact badly with required-check gating) but the script's --diff-base $BASE_SHA flag short-circuits when no submodule pointer changed, so the typical-PR overhead is ~1 second.

Opt-out and branch fixes

While building this I found two pre-existing issues on master that the check surfaces:

  • external/imgui is pinned at 4c902849 ("correct include case"), which is a slang-local patch on top of vendored imgui v1.68 and is not on upstream master. This is a legitimate vendored fork, not a mistake. Added a slang-skip-pin-check = true flag in .gitmodules to opt this submodule out of branch reachability. The script still verifies the pinned SHA is fetchable from the URL, so a typo or upstream history rewrite would still be caught.
  • external/lua is pinned at tag v5.4.8, which lives on the upstream's v5.4 maintenance branch (not master). Added branch = v5.4 to track the right branch, similar to how external/cmark already uses branch = gfm.

The opt-out flag should be used sparingly. The only legitimate use case is a vendored fork like imgui where the pinned commit intentionally isn't on any upstream branch.

Follow-ups

  • Once this lands, please add Check Submodule Pointers to required status checks on master. I'd appreciate help with this since it needs admin access to repo settings.
  • The same check should be added to sibling repositories (slang-rhi, etc.) — happy to file follow-up issues / PRs once this approach is settled here.

Add a CI check that fails the build if any submodule pointer in
.gitmodules points at a commit that is not reachable from the upstream
submodule's tracked branch (the `branch =` override if present, otherwise
the remote's default branch).

Motivated by the slang-rhi haaggarwal/SER incident: PRs only show the
submodule pointer diff, so reviewers cannot tell when a pin moves to a
developer branch instead of mainline. This check enforces the invariant
automatically.

extras/check-submodule-commits.sh resolves each pin via `git ls-tree
HEAD` so it works without checking out submodules. It fetches each
submodule into a temporary bare repo with progressive depth (50 -> 500
-> full) and uses `git merge-base --is-ancestor` to verify reachability.
A `--diff-base <ref>` flag limits work to submodules that actually moved
between the base and HEAD; without it, every submodule is checked
(useful for ad-hoc local invocation).

The workflow runs on pull_request and merge_group, and intentionally
does not use a `paths:` filter — GitHub treats path-skipped jobs as
"not run", which breaks required-check gating. The `--diff-base` flag
short-circuits when no submodule pointer changed.

Two adjustments to keep the initial rollout clean against master:

- Opt-out: a new `submodule.<name>.slang-skip-pin-check = true` key in
  .gitmodules disables branch-reachability for that submodule. The
  script still verifies the pinned SHA is fetchable from the URL, so
  typos and rewritten history are still caught. Applied to
  external/imgui (vendored fork of v1.68 carrying a slang-local patch
  whose commit isn't on upstream master).

- Branch override for external/lua: pinned at tag v5.4.8, which lives on
  the v5.4 maintenance branch, not master. Set `branch = v5.4` in
  .gitmodules so the check tracks the right branch.

Fixes shader-slang#9336
@jkiviluoto-nv jkiviluoto-nv requested a review from a team as a code owner May 5, 2026 19:42
@jkiviluoto-nv jkiviluoto-nv requested review from bmillsNV and removed request for a team May 5, 2026 19:42
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds CI checks and a verification script to ensure git submodule pointers reference SHAs reachable from their tracked remote branches, with opt-out support. Updates .gitmodules to record branch overrides and skip flags, creates a Bash validation script with helpers for bare-repo fetching and ancestry checking, and integrates a GitHub Actions workflow that runs checks on pull requests and merge-group events.

Changes

Submodule Pointer Validation

Layer / File(s) Summary
Configuration and opt-out metadata
.gitmodules
Adds slang-skip-pin-check = true flags and comments for external/imgui and external/spirv-tools submodules, and specifies branch = v5.4 for external/lua with explanatory comments.
Script initialization and CLI interface
extras/check-submodule-commits.sh
Adds script header, usage documentation, command-line argument parsing for --diff-base and --help, validation of .gitmodules presence, and temporary directory setup with cleanup trap.
Verification helper functions
extras/check-submodule-commits.sh
Implements resolve_default_branch() using git ls-remote, ensure_bare_repo() for per-URL bare repositories, is_ancestor() for SHA ancestry testing, verify_sha_exists() for SHA existence validation, and verify_reachable() with progressive shallow fetches (depths 50, 500) and unshallow fallback.
Submodule iteration, filtering, and reporting
extras/check-submodule-commits.sh
Main loop reads submodule names from .gitmodules, applies slang-skip-pin-check opt-out and --diff-base filtering, invokes verification functions per submodule, accumulates results, prints summary with detailed failure reasons, and exits nonzero on failures.
CI workflow orchestration
.github/workflows/check-submodules.yml
Adds "Check Submodule Pointers" workflow triggered on pull_request (targeting master) and merge_group events; skips draft PRs, checks out full history, computes BASE_SHA from PR base or merge-group context, and runs the validation script with --diff-base.

Sequence Diagram(s)

sequenceDiagram
  participant PullRequest
  participant GitHubActions
  participant check-submodule-commits.sh
  participant .gitmodules
  participant RemoteGitRepo
  PullRequest->>GitHubActions: trigger on pull_request or merge_group
  GitHubActions->>check-submodule-commits.sh: run with BASE_SHA and --diff-base
  check-submodule-commits.sh->> .gitmodules: read submodule path/url/branch/skip flags
  check-submodule-commits.sh->>RemoteGitRepo: git ls-remote / fetch pin history
  RemoteGitRepo-->>check-submodule-commits.sh: branch and commit reachability data
  check-submodule-commits.sh-->>GitHubActions: pass or fail summary
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hop through pins and branches bright,
To guard each submodule through the night.
With bash and checks, the path is clear,
No stray commit shall sneak in here!
✨🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly describes the new submodule-pin CI check.
Linked Issues check ✅ Passed The new script and workflow reject submodule pins not reachable from the tracked branch, matching #9336's CI requirement.
Out of Scope Changes check ✅ Passed All changes support the submodule-pin verification flow or its documented exceptions, with no obvious unrelated additions.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3de66caf-9068-42cb-9964-890736e4ac4e

📥 Commits

Reviewing files that changed from the base of the PR and between b7f3dbb and dfa19a5.

📒 Files selected for processing (3)
  • .github/workflows/check-submodules.yml
  • .gitmodules
  • extras/check-submodule-commits.sh

Comment thread .github/workflows/check-submodules.yml Outdated
Comment thread extras/check-submodule-commits.sh Outdated
@jkwak-work

Copy link
Copy Markdown
Collaborator

I think this is against to the recommended workflow.
As an example, when there is a bug fix in SPIRV-Tools, we upstream a PR and use the commit on our slang/master branch without waiting for the PR to be merged to SPIRV-Tools main branch.
This is a common and recommended practice for both SPIRV-Tools and slang-rhi.
It helps us to avoid chicken-and-egg problem as well as faster iterations.

Add an explicit `permissions: contents: read` block on the
check-submodules job so the GITHUB_TOKEN granted to the run carries
only the scope the script actually needs (a checkout). Without this,
PR-triggered jobs inherit the repository's default token scopes,
which is broader than necessary for a workflow that performs no
writes.

Addresses CodeRabbit review feedback on PR shader-slang#11063.
…s.sh

Pass `--default ''` to the `git config` reads of `submodule.<name>.path`
and `submodule.<name>.url` so a missing key returns an empty string
rather than triggering `set -e` and aborting the script before the
existing `if [[ -z "$path" || -z "$url" ]]` guard can emit a warning
and skip the malformed entry. Matches the `--default ''` pattern
already used for the optional `branch` and `slang-skip-pin-check`
keys two lines below.

Addresses CodeRabbit review feedback on PR shader-slang#11063.
Comment thread .github/workflows/check-submodules.yml Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef89f7d5-b492-4242-acf8-e4e80133f0df

📥 Commits

Reviewing files that changed from the base of the PR and between dfa19a5 and f0ae06f.

📒 Files selected for processing (2)
  • .github/workflows/check-submodules.yml
  • extras/check-submodule-commits.sh

Comment thread .github/workflows/check-submodules.yml Outdated
@jkiviluoto-nv

Copy link
Copy Markdown
Contributor Author

I think this is against to the recommended workflow. As an example, when there is a bug fix in SPIRV-Tools, we upstream a PR and use the commit on our slang/master branch without waiting for the PR to be merged to SPIRV-Tools main branch. This is a common and recommended practice for both SPIRV-Tools and slang-rhi. It helps us to avoid chicken-and-egg problem as well as faster iterations.

@jhelferty-nv as original issue #9336 author could comment - do we need this or not? If above quote would mean we have more expections from the rule than cases where this check actually guards CI, let's close both the issue and this PR.

Comment thread extras/check-submodule-commits.sh
@jhelferty-nv

Copy link
Copy Markdown
Contributor

I think this is against to the recommended workflow. As an example, when there is a bug fix in SPIRV-Tools, we upstream a PR and use the commit on our slang/master branch without waiting for the PR to be merged to SPIRV-Tools main branch. This is a common and recommended practice for both SPIRV-Tools and slang-rhi. It helps us to avoid chicken-and-egg problem as well as faster iterations.

@jhelferty-nv as original issue #9336 author could comment - do we need this or not? If above quote would mean we have more expections from the rule than cases where this check actually guards CI, let's close both the issue and this PR.

@jkwak-work I'm fine with allowing that sort of exception. What I want to avoid is having commits pointing to arbitrary developers' commits, so we can avoid the exact following scenario:

  • developer working on changes to slang and slang-rhi
  • slang-rhi change is prepared on developer-repo/work branch
  • slang PR points the slang-rhi submodule at a commit on developer-repo/work branch
  • slang change gets reviewed; reviewer doesn't realize that hash points to developer-repo instead of checked-in commit on shader-slang repository, and approves the PR, which is now merged
  • slang-rhi undergoes additional iterations before it's checked in

At this point, slang is pointing at a version of slang-rhi that never went through quality gates, and may in fact differ from what was eventually checked into slang-rhi. If there are unintended changes in slang-rhi, slang may start to depend on them.

The other way we could try to handle this is by making it an agent responsibility to check the git commit hashes, and report on which repository and branch they come from. (It seemed cheaper/easier to just enforce it with automation, and maybe a few allow lists, though)

@jkwak-work

Copy link
Copy Markdown
Collaborator

I think this is against to the recommended workflow. As an example, when there is a bug fix in SPIRV-Tools, we upstream a PR and use the commit on our slang/master branch without waiting for the PR to be merged to SPIRV-Tools main branch. This is a common and recommended practice for both SPIRV-Tools and slang-rhi. It helps us to avoid chicken-and-egg problem as well as faster iterations.

@jhelferty-nv as original issue #9336 author could comment - do we need this or not? If above quote would mean we have more expections from the rule than cases where this check actually guards CI, let's close both the issue and this PR.

@jkwak-work I'm fine with allowing that sort of exception. What I want to avoid is having commits pointing to arbitrary developers' commits, so we can avoid the exact following scenario:

  • developer working on changes to slang and slang-rhi
  • slang-rhi change is prepared on developer-repo/work branch
  • slang PR points the slang-rhi submodule at a commit on developer-repo/work branch
  • slang change gets reviewed; reviewer doesn't realize that hash points to developer-repo instead of checked-in commit on shader-slang repository, and approves the PR, which is now merged
  • slang-rhi undergoes additional iterations before it's checked in

At this point, slang is pointing at a version of slang-rhi that never went through quality gates, and may in fact differ from what was eventually checked into slang-rhi. If there are unintended changes in slang-rhi, slang may start to depend on them.

The other way we could try to handle this is by making it an agent responsibility to check the git commit hashes, and report on which repository and branch they come from. (It seemed cheaper/easier to just enforce it with automation, and maybe a few allow lists, though)

I see.
If we are checking the "repo" not the branch, that sounds good.
We don't want to accidently point to a forked/developer repo.

@jhelferty-nv

jhelferty-nv commented May 6, 2026

Copy link
Copy Markdown
Contributor

I see. If we are checking the "repo" not the branch, that sounds good. We don't want to accidently point to a forked/developer repo.

Right. And even on the main repo, shader-slang team members can push developer branches. Hence the suggestion to always enforce being a commit from main/master branch. That said, if this is overly contentious, I'd be ok with an initial half-way measure of just disallowing arbitrary developer repositories as you suggest. Some protection is better than none.

@jkwak-work

Copy link
Copy Markdown
Collaborator

I discussed with @jhelferty-nv on the meeting.
Let's go with checking both the repo address and the branch name for now.
We will need to make spirv-tools an exception to the rule but it can come later.

We routinely pin SPIRV-Tools to a fix that has been upstreamed as a PR but
not yet merged to KhronosGroup main, to avoid a chicken-and-egg wait. Such a
commit is deliberately not yet on the tracked branch, so the branch-reachability
rule would reject it.

Opt SPIRV-Tools out via the existing slang-skip-pin-check mechanism: the branch
check is skipped, but the pinned SHA is still verified to be fetchable from the
official KhronosGroup URL, which still catches pins that accidentally reference
a developer fork.

Agreed with jhelferty-nv and jkwak-work in PR shader-slang#11063 review discussion.
@jkiviluoto-nv

Copy link
Copy Markdown
Contributor Author

Added the SPIRV-Tools exception we agreed on (@jhelferty-nv / @jkwak-work), and merged in the latest master.

SPIRV-Tools exception (22a59175d): implemented via the existing slang-skip-pin-check mechanism rather than a new code path. For external/spirv-tools the branch-reachability check is now skipped — so a fix that's been upstreamed as a Khronos PR but not yet merged to their main is allowed (the chicken-and-egg case @jkwak-work raised). The pinned SHA is still verified to be fetchable from the official KhronosGroup/SPIRV-Tools.git URL, which preserves the protection @jhelferty-nv asked for against pins that accidentally reference a developer fork.

Verified locally — the check reports for spirv-tools:

INFO: 'external/spirv-tools' skipping branch check (opted out via submodule.external/spirv-tools.slang-skip-pin-check); verifying SHA ... is fetchable.
  PASS: ... is fetchable from https://github.com/KhronosGroup/SPIRV-Tools.git.

Submodules checked: 16  skipped: 0  failed: 0

Also merged upstream/master (was 291 behind) — clean, no conflicts, and re-ran the check green afterward. Labeled pr: non-breaking.

@jhelferty-nv this should address the change request — could you take another look when you have a moment?

@jkiviluoto-nv jkiviluoto-nv enabled auto-merge June 23, 2026 13:25

@jkwak-work jkwak-work left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

Comment thread .gitmodules Outdated
Comment thread .gitmodules Outdated
The lua pin (3fe7be9, v5.4.7-7-g3fe7be9) is reachable from lua's v5.4
maintenance branch but not from its default branch 'master'. The 'branch =
v5.4' override is therefore required for the pin check to resolve the correct
branch; without it the check would fall back to 'master' and fail. Expand the
comment to state this, and correct the stale 'tag v5.4.8' note (the pin is 7
commits past v5.4.7, not v5.4.8).
verify_reachable in check-submodule-commits.sh can escalate to a full
`git fetch --unshallow` per submodule against its upstream remote. Without
a job timeout, a slow or unresponsive remote could hold a runner until
GitHub's 360-minute default deadline. Bound the job at 20 minutes, which
still leaves ample headroom for full-history fetches of every tracked
submodule.

Addresses CodeRabbit review feedback on PR shader-slang#11063.
The submodule pin check resolved a submodule's tracked ref (the `branch =`
value in .gitmodules, or the remote default) only as a branch
(refs/heads/<name>). But a submodule may legitimately track a release tag
instead: external/fast_float tracks `v8.2.7`, which exists upstream only as
refs/tags/v8.2.7 with no same-named branch. The branch-only fetch could never
resolve such a ref, so the pin was reported unreachable even though it is in
fact pinned to an immutable tag — a stronger guarantee than a branch pin.

Factor the depth-escalating fetch into verify_reachable_from_ref, which takes
a fully-qualified source ref, and have verify_reachable try the branch form
first and fall back to the tag form. A commit that is on neither the branch
nor the tag still fails, so real developer-branch pins are still caught.

Updates the script header, INFO/PASS/ERROR wording, and the fix hint to say
"branch or tag" instead of "branch".
@jkiviluoto-nv

Copy link
Copy Markdown
Contributor Author

@jhelferty-nv friendly nudge to revisit — your CHANGES_REQUESTED from May 6 predates the resolution of the points it raised:

  • Your gh api .../compare suggestion: @jkwak-work weighed in (Jun 23) preferring to avoid gh (auth-token handling + gh may not be installed when the script runs locally) and gave an LGTM on the fetch-based approach.
  • The CodeRabbit nits are addressed: permissions: contents: read + a 20-minute timeout-minutes bound on the job.
  • Rebased onto current master via merge. That surfaced a real gap the check itself had: master's new external/fast_float tracks v8.2.7, which is a tag, not a branch — the check only fetched refs/heads/<ref> so it reported the pin unreachable. Fixed by also accepting refs/tags/<ref> (a commit on neither the branch nor the tag still fails). All 17 submodules now pass locally.

Could you dismiss/update the stale review when you have a moment? Thanks!

Comment thread .gitmodules Outdated
Comment thread .github/workflows/check-submodules.yml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
extras/check-submodule-commits.sh (1)

210-213: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not trust PR-modified submodule metadata when deciding what to validate.

The check reads url, branch, and slang-skip-pin-check from HEAD, then --diff-base skips whenever the gitlink SHA is unchanged. A PR can therefore change only .gitmodules metadata and be skipped, or change the URL to a fork and validate the new pin against that fork’s branch. Load the base .gitmodules too, include metadata changes in the skip decision, and reject or explicitly require review for URL changes before using the HEAD URL as the trust anchor.

Also applies to: 237-246


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3dfa8021-ef3d-4872-972c-fd0a361ef873

📥 Commits

Reviewing files that changed from the base of the PR and between 79640b2 and b860e88.

📒 Files selected for processing (3)
  • .github/workflows/check-submodules.yml
  • .gitmodules
  • extras/check-submodule-commits.sh

Comment thread extras/check-submodule-commits.sh Outdated
imgui was updated to v1.92.8 (8936b58f) on master by shader-slang#11713, and that
commit is reachable from imgui's upstream master. The vendored-v1.68
skip is stale, so drop the slang-skip-pin-check opt-out and let imgui go
through the normal branch reachability check like every other tracked
submodule.
An opt-out submodule (slang-skip-pin-check = true) only reaches the
failure list when its pinned SHA is not fetchable from the URL at all;
its branch/tag reachability is never checked. The generic 'not reachable
from tracked ref' remediation is misleading for that case, so print
URL/SHA/rewritten-history guidance when the ref is '<opted out>'.
check-submodules is not a required status check, so a path-skipped job
on an unrelated PR is harmless. Add a pull_request paths: filter
(external/**, .gitmodules, the workflow, the script) so the job only
spins up a runner and fetches submodule history when a submodule pointer
could have changed. merge_group keeps running over all submodules as a
final gate (GitHub ignores paths: on merge_group). This makes the
per-PR --diff-base short-circuit redundant, so drop it from the workflow
invocation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr: non-breaking PRs without breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: Require submodule commits be on main

4 participants