diff --git a/.github/workflows/manifest-diff.yml b/.github/workflows/manifest-diff.yml
index 515f86cf199..66657e696aa 100644
--- a/.github/workflows/manifest-diff.yml
+++ b/.github/workflows/manifest-diff.yml
@@ -4,37 +4,72 @@ on:
workflow_dispatch:
inputs:
start_ref:
- description: 'Start commit SHA or workflow run ID (required when using workflow mode or commit SHA mode without find-last-successful)'
+ description: 'Start commit SHA or workflow run ID. Used when neither pr_base_ref nor find_last_run is set (start resolution precedence: pr_base_ref > find_last_run > start_ref).'
required: false
type: string
end_ref:
- description: 'End commit SHA or workflow run ID'
+ description: 'End commit SHA (or, with workflow_mode=true, a workflow run ID).'
required: true
type: string
workflow_mode:
- description: 'Treat inputs as workflow run IDs instead of commit SHAs'
+ description: 'Interpret start_ref and end_ref as workflow run IDs instead of commit SHAs (pr_base_ref and find_last_run are unaffected).'
required: false
type: boolean
default: false
- find_last_successful:
- description: 'Workflow file to find last successful run (e.g., ci_nightly.yml)'
+ find_last_run:
+ description: 'Workflow filename (e.g. multi_arch_ci.yml). When set, start_ref is resolved as the head SHA of that workflow''s most recent run on `branch` that concluded with success or failure (cancelled / skipped / in-progress runs are ignored).'
required: false
type: string
+ pr_base_ref:
+ description: 'PR base branch name (e.g. main). When set, start_ref is resolved as the merge-base between end_ref and the named branch via the GitHub Compare API — rebase-safe, works on rewritten PR histories.'
+ required: false
+ type: string
+ branch:
+ description: 'Branch to scope find_last_run lookups against (default: main). Only consulted when find_last_run is set; ignored otherwise.'
+ required: false
+ type: string
+ default: 'main'
+ repository:
+ description: 'Repository to check out (default: the repository hosting this workflow run).'
+ required: false
+ type: string
+ default: ''
+ ref:
+ description: 'Git ref to check out (default: the ref hosting this workflow run).'
+ required: false
+ type: string
+ default: ''
workflow_call:
inputs:
start_ref:
required: false
type: string
end_ref:
- required: true
+ required: false
type: string
+ default: ''
workflow_mode:
required: false
type: boolean
default: false
- find_last_successful:
+ find_last_run:
+ required: false
+ type: string
+ pr_base_ref:
+ required: false
+ type: string
+ branch:
required: false
type: string
+ default: 'main'
+ repository:
+ required: false
+ type: string
+ default: ''
+ ref:
+ required: false
+ type: string
+ default: ''
permissions:
contents: read
@@ -42,36 +77,61 @@ permissions:
jobs:
generate-report:
name: Generate Manifest Diff Report
- runs-on: ubuntu-24.04
+ runs-on: ${{ github.repository_owner == 'ROCm' && 'azure-linux-scale-rocm' || 'ubuntu-24.04' }}
+ continue-on-error: true
permissions:
id-token: write
contents: read
+ container:
+ image: ubuntu:24.04
+ options: -v /runner/config:/home/awsconfig/
+ env:
+ AWS_SHARED_CREDENTIALS_FILE: /home/awsconfig/credentials.ini
steps:
+ - name: Install bootstrap packages
+ run: |
+ apt-get update
+ apt-get install -y git ca-certificates curl unzip
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
+ repository: ${{ inputs.repository || github.repository }}
+ ref: ${{ inputs.ref || '' }}
fetch-depth: 0
+ - name: Set up Python
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
+ with:
+ python-version: '3.12'
+
+ - name: Install AWS CLI
+ run: |
+ ./dockerfiles/install_awscli.sh
+
+ - name: Adjust git config
+ run: |
+ git config --global --add safe.directory $PWD
+
- name: Generate manifest diff report
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python3 build_tools/generate_manifest_diff_report.py \
- --end "${{ inputs.end_ref }}" \
- --start "${{ inputs.start_ref || '' }}" \
- --find-last-successful "${{ inputs.find_last_successful || '' }}" \
+ --end "${{ inputs.end_ref || (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha }}" \
+ --start "${{ inputs.start_ref || (github.event_name == 'push' && github.event.before) || '' }}" \
+ --find-last-run "${{ inputs.find_last_run }}" \
+ --pr-base-ref "${{ inputs.pr_base_ref || (github.event_name == 'pull_request' && github.event.pull_request.base.ref) || '' }}" \
+ --branch "${{ inputs.branch }}" \
${{ inputs.workflow_mode && '--workflow-mode' || '' }} \
--output-dir reports
- - name: Configure AWS Credentials
- if: ${{ github.repository == 'ROCm/TheRock' }}
- uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1
- with:
- aws-region: us-east-2
- role-to-assume: arn:aws:iam::692859939525:role/therock-ci
+ - name: Configure AWS credentials for artifact uploads
+ if: ${{ always() }}
+ uses: ./.github/actions/configure_aws_artifacts_credentials
- name: Upload Report to S3
- if: ${{ github.repository == 'ROCm/TheRock' }}
+ if: ${{ always() }}
run: |
python3 build_tools/github_actions/upload_test_report_script.py \
--run-id ${{ github.run_id }} \
diff --git a/.github/workflows/multi_arch_ci.yml b/.github/workflows/multi_arch_ci.yml
index f191268b69c..5a43ac94cff 100644
--- a/.github/workflows/multi_arch_ci.yml
+++ b/.github/workflows/multi_arch_ci.yml
@@ -115,6 +115,13 @@ jobs:
contents: read
id-token: write
+ manifest_diff:
+ name: Manifest Diff
+ uses: ./.github/workflows/manifest-diff.yml
+ permissions:
+ contents: read
+ id-token: write
+
ci_summary:
name: CI Summary
if: always()
diff --git a/build_tools/generate_manifest_diff_report.py b/build_tools/generate_manifest_diff_report.py
index 36fa2102567..6e852d85518 100644
--- a/build_tools/generate_manifest_diff_report.py
+++ b/build_tools/generate_manifest_diff_report.py
@@ -4,14 +4,32 @@
for each component between builds.
Arguments:
- --start Start commit SHA or workflow run ID (required unless using --find-last-successful)
- --end End commit SHA or workflow run ID (required)
- --find-last-successful Workflow file to find last successful run (e.g., 'ci_nightly.yml')
- --workflow-mode Treat --start and --end as workflow run IDs instead of commit SHAs
+ --start Start commit SHA or workflow run ID (required unless using
+ --find-last-run or --pr-base-ref).
+ --end End commit SHA or workflow run ID (required).
+ --find-last-run Workflow filename (e.g., 'multi_arch_ci.yml'). When set,
+ --start is resolved as the head SHA of that workflow's
+ most recent run on --branch that concluded with
+ 'success' or 'failure' (cancelled / skipped /
+ in-progress runs are ignored).
+ --pr-base-ref PR base branch name. When set, --start is resolved as
+ the merge-base between --end and the named branch via
+ the GitHub Compare API. Rebase-safe.
+ --workflow-mode Treat --start and --end as workflow run IDs instead of
+ commit SHAs.
+ --branch Branch to scope --find-last-run lookups against
+ (default: 'main'). Only consulted when --find-last-run
+ is set.
+ --output-dir Directory to write the HTML report into. If unset,
+ falls back to the TheRock root directory.
+
+If no usable start ref can be derived, the script logs the reason and
+exits 0 without writing a report.
Example usage:
python build_tools/generate_manifest_diff_report.py --start abc123 --end def456
- python build_tools/generate_manifest_diff_report.py --end def456 --find-last-successful ci_nightly.yml
+ python build_tools/generate_manifest_diff_report.py --end def456 --find-last-run multi_arch_ci.yml
+ python build_tools/generate_manifest_diff_report.py --end def456 --pr-base-ref main
python build_tools/generate_manifest_diff_report.py --start 12345 --end 67890 --workflow-mode
"""
@@ -34,7 +52,7 @@
from generate_therock_manifest import build_manifest_schema
from github_actions.github_actions_api import (
gha_append_step_summary,
- gha_query_last_successful_workflow_run,
+ gha_query_last_workflow_run,
gha_query_workflow_run_by_id,
gha_send_request,
)
@@ -181,8 +199,21 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
)
parser.add_argument("--end", required=True, help="End workflow ID or commit SHA")
parser.add_argument(
- "--find-last-successful",
- help="Workflow name to find last successful run (e.g., 'ci_nightly.yml')",
+ "--find-last-run",
+ help=(
+ "Workflow filename (e.g. 'multi_arch_ci.yml'). When set, --start "
+ "is resolved as the head SHA of that workflow's most recent run "
+ "on --branch that concluded with 'success' or 'failure' "
+ "(cancelled / skipped / in-progress runs are ignored)."
+ ),
+ )
+ parser.add_argument(
+ "--pr-base-ref",
+ help=(
+ "PR base branch name. When set, --start is resolved as the "
+ "merge-base between --end and this branch via the GitHub Compare "
+ "API. Rebase-safe."
+ ),
)
parser.add_argument(
"--workflow-mode",
@@ -192,7 +223,10 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
parser.add_argument(
"--branch",
default="main",
- help="Branch to search for last successful workflow run (default: main)",
+ help=(
+ "Branch to scope --find-last-run lookups against (default: main). "
+ "Only consulted when --find-last-run is set; ignored otherwise."
+ ),
)
parser.add_argument(
"--output-dir",
@@ -214,42 +248,68 @@ def _optional_str(val: str | None) -> str | None:
return s if s else None
-def resolve_commits(args: argparse.Namespace) -> tuple[str, str]:
- """Resolve start and end commit SHAs from arguments."""
+def resolve_commits(
+ args: argparse.Namespace,
+) -> tuple[str | None, str | None]:
+ """Resolve start and end commit SHAs from arguments.
+
+ Returns ``(None, None)`` when --find-last-run finds no prior matching
+ run (graceful empty); caller should exit 0. Other API errors (e.g.
+ Compare API 404 on --pr-base-ref) propagate as GitHubAPIError; the
+ workflow's continue-on-error gate handles those.
+ """
start = _optional_str(args.start)
- find_last = _optional_str(args.find_last_successful)
+ find_last = _optional_str(args.find_last_run)
+ pr_base = _optional_str(args.pr_base_ref)
end = _optional_str(args.end)
- if start is None and find_last is None:
- raise ValueError(
- "--start is required unless --find-last-successful is provided"
- )
if end is None:
raise ValueError("--end is required")
+ if start is None and find_last is None and pr_base is None:
+ raise ValueError(
+ "--start is required unless --find-last-run or --pr-base-ref is provided"
+ )
- therock_repo_full = f"{ROCM_ORG}/{THEROCK_REPO}"
+ therock = f"{ROCM_ORG}/{THEROCK_REPO}"
- # Resolve start commit
- if find_last is not None:
- last_run = gha_query_last_successful_workflow_run(
- therock_repo_full, find_last, branch=args.branch
+ # Resolve end first; --pr-base-ref needs end_sha to compute the merge-base.
+ if args.workflow_mode:
+ end_sha = gha_query_workflow_run_by_id(therock, end).get("head_sha")
+ else:
+ end_sha = end
+
+ # Resolve start, in priority order: --pr-base-ref, --find-last-run,
+ # --workflow-mode, then plain --start.
+ if pr_base is not None:
+ compare_url = (
+ f"https://api.github.com/repos/{therock}/compare/{pr_base}...{end_sha}"
+ )
+ compare = gha_send_request(compare_url)
+ start_sha = compare.get("merge_base_commit", {}).get("sha")
+ elif find_last is not None:
+ # Hardcoded to terminal-status: any run that ran to completion (success
+ # or failure) is acceptable — devs comparing to "the last run that
+ # actually ran" don't care whether it was green or red, only that it
+ # wasn't cancelled / skipped / in-progress.
+ accepted = {"success", "failure"}
+ last_run = gha_query_last_workflow_run(
+ therock,
+ find_last,
+ branch=args.branch,
+ accepted_statuses=accepted,
)
- if not last_run:
- raise ValueError(f"No previous successful run found for {find_last}")
+ if last_run is None:
+ print(
+ f" [empty] No prior run of {find_last} on {args.branch} with "
+ f"conclusion in {sorted(accepted)} (likely a first-ever run)."
+ )
+ return None, None
start_sha = last_run["head_sha"]
elif args.workflow_mode:
- workflow_info = gha_query_workflow_run_by_id(therock_repo_full, start)
- start_sha = workflow_info.get("head_sha")
+ start_sha = gha_query_workflow_run_by_id(therock, start).get("head_sha")
else:
start_sha = start
- # Resolve end commit
- if args.workflow_mode:
- workflow_info = gha_query_workflow_run_by_id(therock_repo_full, end)
- end_sha = workflow_info.get("head_sha")
- else:
- end_sha = end
-
return start_sha, end_sha
@@ -1423,11 +1483,11 @@ def main(argv: list[str] | None = None) -> int:
"""Main entry point."""
args = parse_args(argv)
start_commit, end_commit = resolve_commits(args)
+ if start_commit is None:
+ return 0
diff = compare_manifests(start_commit, end_commit)
-
- output_dir = args.output_dir
- generate_html_report(diff, output_dir)
+ generate_html_report(diff, args.output_dir)
print("\n=== Generating Step Summary ===")
generate_step_summary(diff)
diff --git a/build_tools/github_actions/configure_ci_path_filters.py b/build_tools/github_actions/configure_ci_path_filters.py
index 941c4520f55..3e2ac7ad667 100644
--- a/build_tools/github_actions/configure_ci_path_filters.py
+++ b/build_tools/github_actions/configure_ci_path_filters.py
@@ -193,6 +193,7 @@ def is_ci_run_required(paths: Optional[Iterable[str]]) -> bool:
"multi_arch_build_windows_artifacts.yml",
"multi_arch_build_wsl_rocdxg_artifacts.yml",
"setup_multi_arch.yml",
+ "manifest-diff.yml",
"test_artifacts_structure.yml",
"test_native_linux_packages_install.yml",
# both
diff --git a/build_tools/github_actions/github_actions_api.py b/build_tools/github_actions/github_actions_api.py
index 4f6aaa6c978..2964bccc7f4 100644
--- a/build_tools/github_actions/github_actions_api.py
+++ b/build_tools/github_actions/github_actions_api.py
@@ -474,29 +474,32 @@ def gha_query_workflow_runs_for_commit(
return runs
-def gha_query_last_successful_workflow_run(
+def gha_query_last_workflow_run(
github_repository: str = "ROCm/TheRock",
workflow_name: str = "multi_arch_ci.yml",
branch: str = "main",
+ accepted_statuses: set[str] | None = None,
) -> dict | None:
- """Find the last successful run of a specific workflow on the specified branch.
+ """Find the most recent run of a workflow on ``branch`` whose conclusion
+ is in ``accepted_statuses`` (default ``{"success"}``).
- Args:
- github_repository: Repository in format "owner/repo"
- workflow_name: Name of the workflow file (e.g., "ci_nightly.yml")
- branch: Branch to filter by (defaults to "main")
+ Filters client-side from the most-recent 100 runs because the
+ workflow-runs endpoint accepts at most one ``status=`` filter.
- Returns:
- The full workflow run object of the most recent successful run on the specified branch,
- or None if no successful runs are found.
+ Returns the matching run dict, or ``None`` if none of the last ~100
+ runs on ``branch`` has an accepted conclusion.
"""
- # Use GitHub API query parameters to pre-filter for successful runs on the specified branch
- url = f"https://api.github.com/repos/{github_repository}/actions/workflows/{workflow_name}/runs?status=success&branch={branch}&per_page=100&sort=created&direction=desc"
+ if accepted_statuses is None:
+ accepted_statuses = {"success"}
+ url = (
+ f"https://api.github.com/repos/{github_repository}"
+ f"/actions/workflows/{workflow_name}/runs"
+ f"?branch={branch}&per_page=100&sort=created&direction=desc"
+ )
response = gha_send_request(url)
-
- # Return the first (most recent) successful run
- if response and response.get("workflow_runs"):
- return response["workflow_runs"][0]
+ for run in response.get("workflow_runs", []) if response else []:
+ if run.get("conclusion") in accepted_statuses:
+ return run
return None
diff --git a/build_tools/github_actions/tests/github_actions_api_test.py b/build_tools/github_actions/tests/github_actions_api_test.py
index 348e563204a..595d6446535 100644
--- a/build_tools/github_actions/tests/github_actions_api_test.py
+++ b/build_tools/github_actions/tests/github_actions_api_test.py
@@ -16,7 +16,7 @@
GitHubAPI,
GitHubAPIError,
gha_load_github_event,
- gha_query_last_successful_workflow_run,
+ gha_query_last_workflow_run,
gha_query_recent_branch_commits,
gha_query_workflow_run_by_id,
gha_query_workflow_runs_for_commit,
@@ -556,26 +556,34 @@ def test_gha_query_workflow_runs_for_commit_sorts_by_created_at(self):
self.assertEqual(runs[1]["id"], 1, "Older run should be second")
@_skip_unless_authenticated_github_api_is_available
- def test_gha_query_last_successful_workflow_run(self):
- """Test querying for the last successful workflow run on a branch."""
+ def test_gha_query_last_workflow_run(self):
+ """Test querying for the last workflow run on a branch."""
# Test successful run found on main branch
- result = gha_query_last_successful_workflow_run(
- "ROCm/TheRock", "ci_nightly.yml", "main"
- )
+ result = gha_query_last_workflow_run("ROCm/TheRock", "ci_nightly.yml", "main")
self.assertIsNotNone(result)
self.assertEqual(result["head_branch"], "main")
self.assertEqual(result["conclusion"], "success")
self.assertIn("id", result)
+ # Test multi-status set: accept success or failure
+ result = gha_query_last_workflow_run(
+ "ROCm/TheRock",
+ "ci_nightly.yml",
+ "main",
+ accepted_statuses={"success", "failure"},
+ )
+ self.assertIsNotNone(result)
+ self.assertIn(result["conclusion"], {"success", "failure"})
+
# Test no matching branch - should return None
- result = gha_query_last_successful_workflow_run(
+ result = gha_query_last_workflow_run(
"ROCm/TheRock", "ci_nightly.yml", "nonexistent-branch-12345"
)
self.assertIsNone(result)
# Test non-existent workflow - should raise an exception
with self.assertRaises(Exception):
- gha_query_last_successful_workflow_run(
+ gha_query_last_workflow_run(
"ROCm/TheRock", "nonexistent_workflow_12345.yml", "main"
)
diff --git a/build_tools/tests/generate_manifest_diff_report_test.py b/build_tools/tests/generate_manifest_diff_report_test.py
index 4d725844f42..4ea47198626 100644
--- a/build_tools/tests/generate_manifest_diff_report_test.py
+++ b/build_tools/tests/generate_manifest_diff_report_test.py
@@ -221,8 +221,8 @@ def test_workflow_mode_resolves_both_commits(self):
"generate_manifest_diff_report.gha_query_workflow_run_by_id"
) as mock_query:
mock_query.side_effect = [
+ {"head_sha": "789xyz000111"}, # end workflow (resolved first)
{"head_sha": "abc123def456"}, # start workflow
- {"head_sha": "789xyz000111"}, # end workflow
]
start_sha, end_sha = resolve_commits(args)
@@ -230,22 +230,89 @@ def test_workflow_mode_resolves_both_commits(self):
self.assertEqual(end_sha, "789xyz000111")
self.assertEqual(mock_query.call_count, 2)
- def test_find_last_successful_resolves_start(self):
- """--find-last-successful finds last successful run for start commit."""
- args = parse_args(
- ["--end", "def456", "--find-last-successful", "multi_arch_ci.yml"]
- )
+ def test_find_last_run_resolves_start(self):
+ """--find-last-run finds the most recent matching run for start commit."""
+ args = parse_args(["--end", "def456", "--find-last-run", "multi_arch_ci.yml"])
with mock.patch(
- "generate_manifest_diff_report.gha_query_last_successful_workflow_run"
+ "generate_manifest_diff_report.gha_query_last_workflow_run"
) as mock_query:
- mock_query.return_value = {"head_sha": "last_successful_sha"}
+ mock_query.return_value = {"head_sha": "last_matching_sha"}
start_sha, end_sha = resolve_commits(args)
- self.assertEqual(start_sha, "last_successful_sha")
+ self.assertEqual(start_sha, "last_matching_sha")
self.assertEqual(end_sha, "def456")
mock_query.assert_called_once()
+ def test_find_last_run_uses_terminal_statuses(self):
+ """--find-last-run hardcodes accepted statuses to {success, failure}."""
+ args = parse_args(["--end", "def456", "--find-last-run", "ci.yml"])
+
+ with mock.patch(
+ "generate_manifest_diff_report.gha_query_last_workflow_run"
+ ) as mock_query:
+ mock_query.return_value = {"head_sha": "abc"}
+ resolve_commits(args)
+
+ _, kwargs = mock_query.call_args
+ self.assertEqual(kwargs["accepted_statuses"], {"success", "failure"})
+
+ def test_pr_base_ref_resolves_start_via_compare(self):
+ """--pr-base-ref resolves start as the merge-base via the Compare API."""
+ args = parse_args(["--end", "deadbeef", "--pr-base-ref", "main"])
+
+ with mock.patch(
+ "generate_manifest_diff_report.gha_send_request"
+ ) as mock_request:
+ mock_request.return_value = {"merge_base_commit": {"sha": "base_sha_xyz"}}
+ start_sha, end_sha = resolve_commits(args)
+
+ self.assertEqual(start_sha, "base_sha_xyz")
+ self.assertEqual(end_sha, "deadbeef")
+ # Verify the URL hit Compare API with base...end ordering.
+ called_url = mock_request.call_args.args[0]
+ self.assertIn("/compare/main...deadbeef", called_url)
+
+ def test_find_last_run_no_match_returns_none(self):
+ """--find-last-run with no matching prior run → (None, None)."""
+ args = parse_args(["--end", "def456", "--find-last-run", "ci.yml"])
+ with mock.patch(
+ "generate_manifest_diff_report.gha_query_last_workflow_run",
+ return_value=None,
+ ):
+ self.assertEqual(resolve_commits(args), (None, None))
+
+ def test_pr_base_ref_takes_precedence_over_find_last_run(self):
+ """When both --pr-base-ref and --find-last-run are set, Compare wins.
+
+ This pins the precedence ladder documented in resolve_commits():
+ pr_base_ref > find_last_run > workflow_mode/start_ref. If a future
+ refactor reorders the branches, this test catches it.
+ """
+ args = parse_args(
+ [
+ "--end",
+ "deadbeef",
+ "--pr-base-ref",
+ "main",
+ "--find-last-run",
+ "ci.yml",
+ ]
+ )
+
+ with mock.patch(
+ "generate_manifest_diff_report.gha_send_request"
+ ) as mock_compare, mock.patch(
+ "generate_manifest_diff_report.gha_query_last_workflow_run"
+ ) as mock_last_run:
+ mock_compare.return_value = {"merge_base_commit": {"sha": "merge_base"}}
+ start_sha, end_sha = resolve_commits(args)
+
+ self.assertEqual(start_sha, "merge_base")
+ self.assertEqual(end_sha, "deadbeef")
+ mock_compare.assert_called_once()
+ mock_last_run.assert_not_called()
+
def test_direct_commit_shas_no_api_calls(self):
"""Direct commit SHAs don't require API calls."""
args = parse_args(["--start", "abc123", "--end", "def456"])
diff --git a/docs/development/ci_overview.md b/docs/development/ci_overview.md
index 5c1a547d8c9..06fa9a8c241 100644
--- a/docs/development/ci_overview.md
+++ b/docs/development/ci_overview.md
@@ -167,6 +167,7 @@ See [workflow_outputs.md](workflow_outputs.md) for the S3 layout structure and [
- [workflow_outputs.md](workflow_outputs.md) - CI output directory structure
- [github_actions_debugging.md](github_actions_debugging.md) - Debugging GitHub Actions
- [ci_behavior_manipulation.md](ci_behavior_manipulation.md) - Controlling CI behavior with labels and inputs
+- [manifest_diff.md](manifest_diff.md) - Manifest diff report (submodule SHA changes between two commits)
## Getting Help
diff --git a/docs/development/manifest_diff.md b/docs/development/manifest_diff.md
new file mode 100644
index 00000000000..30f058ecf43
--- /dev/null
+++ b/docs/development/manifest_diff.md
@@ -0,0 +1,76 @@
+# Manifest Diff Report
+
+This document describes the **manifest diff report** — a CI tool that summarizes which TheRock submodule SHAs changed between two commits. It runs automatically on every multi-arch CI run for `pull_request` and `push` events, and can also be invoked on-demand via direct `workflow_dispatch` of [`manifest-diff.yml`](../../.github/workflows/manifest-diff.yml) or locally on the command line.
+
+## Summary
+
+TheRock is a CMake super-project pinned to a small set of top-level git submodules (`.gitmodules`), two of which (`rocm-libraries`, `rocm-systems`) are themselves superrepos containing further ROCm components under `projects/` and `shared/`. When a change in TheRock or in any of those upstream repos lands, it can be non-obvious which pointer(s) actually moved. The manifest diff report answers that question: for two TheRock commits (a **start** and an **end**), it walks the manifest produced by `generate_therock_manifest.py` — top-level submodules plus superrepo components — and produces an HTML page listing the new commit range on each one, with links back to the upstream repos.
+
+The report is generated by [`build_tools/generate_manifest_diff_report.py`](../../build_tools/generate_manifest_diff_report.py) and uploaded to S3 by the [`manifest-diff.yml`](../../.github/workflows/manifest-diff.yml) reusable workflow.
+
+## How it runs in CI
+
+The multi-arch CI top-level workflow ([`multi_arch_ci.yml`](../../.github/workflows/multi_arch_ci.yml)) hosts the `manifest_diff` job as a top-level sibling. The job has no `needs:`, runs in parallel with `linux_build_and_test` / `windows_build_and_test`, and calls `manifest-diff.yml` with no `with:` block — the reusable workflow derives the **start** and **end** refs from the caller's `github.event` itself, choosing differently depending on what triggered the run.
+
+```mermaid
+flowchart TD
+ A[multi_arch_ci.yml fires] --> B{event_name}
+ B -- pull_request --> P[pr_base_ref = pull_request.base.ref
end_ref = pull_request.head.sha]
+ B -- push --> U[start_ref = github.event.before
end_ref = github.sha]
+ P --> G[generate_manifest_diff_report.py]
+ U --> G
+ G --> R[reports/index.html]
+ R --> O[upload_test_report_script.py]
+ O --> H[S3 index.html + step-summary link]
+```
+
+| Event | Start ref source | End ref source |
+| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
+| `pull_request` | `pr_base_ref` (the PR's base branch). The script calls the GitHub Compare API to get the merge-base, which is rebase-safe and works on rewritten PRs. | `pull_request.head.sha` — the tip of the PR's source branch. |
+| `push` | `github.event.before` — the branch tip before the push. | `github.sha` — the new tip the branch was just moved to. |
+
+To run the report manually with explicit refs, dispatch [`manifest-diff.yml`](../../.github/workflows/manifest-diff.yml) directly — see [Running it manually](#running-it-manually). `multi_arch_ci.yml`'s own `workflow_dispatch` surface intentionally does not expose manifest-diff inputs, so dispatching `multi_arch_ci.yml` is not a supported way to produce a report; the auto-fired `manifest_diff` job will fail (yellow via `continue-on-error`, never red).
+
+The `manifest_diff` job in `multi_arch_ci.yml` is **purely informational** — it runs in parallel with the build/test jobs, never gates them, and is marked `continue-on-error: true` inside `manifest-diff.yml` itself so an API hiccup is reported as a non-blocking warning on the run summary instead of turning the whole CI run red.
+
+### Where the report lives
+
+`manifest-diff.yml`'s upload step calls [`upload_test_report_script.py`](../../build_tools/github_actions/upload_test_report_script.py), which pushes `reports/index.html` under the run's S3 prefix using `manifest-diff` as the "amdgpu family" segment. The same upload script appends a link to `$GITHUB_STEP_SUMMARY`, so the report shows up in the **Summary** tab of the workflow run.
+
+S3 path (base-repo runs):
+
+```
+s3://therock-ci-artifacts/{run_id}-linux/logs/manifest-diff/index.html
+```
+
+On downstream forks the bucket and prefix shift to `therock-ci-artifacts-external` and `-/{run_id}-linux/...` per `WorkflowOutputRoot`. The upload step is non-fatal if the runner has no AWS credentials, so on a fork without the creds mount the report simply isn't uploaded. See [`workflow_outputs.md`](workflow_outputs.md) for the full S3 layout.
+
+## Running it manually
+
+### `workflow_dispatch`
+
+Trigger `TheRock Manifest Diff Report` on the [Actions page](https://github.com/ROCm/TheRock/actions/workflows/manifest-diff.yml). `end_ref` is required; pick exactly one of `pr_base_ref`, `find_last_run`, or `start_ref` to resolve the start (precedence: `pr_base_ref` > `find_last_run` > `start_ref`). See the `description:` fields on the inputs in [`manifest-diff.yml`](../../.github/workflows/manifest-diff.yml) for the authoritative reference.
+
+### Local CLI
+
+See [`generate_manifest_diff_report.py`](../../build_tools/generate_manifest_diff_report.py) and run with `--help` for usage. Set `GITHUB_TOKEN` (any token with `public_repo` read scope) before running to avoid GitHub's 60 req/hr unauthenticated rate limit.
+
+## Out of scope
+
+External orchestrator workflows in `rocm-libraries` / `rocm-systems` that drive TheRock's reusable workflows via `setup_multi_arch.yml`'s `external_repo` input, and the rockrel release-driver flow (via `multi_arch_release.yml`), currently produce no manifest-diff; extending the report to those callers is tracked in #5219.
+
+## Code map
+
+| File | Role |
+| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
+| [`.github/workflows/manifest-diff.yml`](../../.github/workflows/manifest-diff.yml) | Reusable workflow: derives refs from caller event, runs script, uploads. |
+| [`.github/workflows/multi_arch_ci.yml`](../../.github/workflows/multi_arch_ci.yml) | Hosts the `manifest_diff` sibling job that calls `manifest-diff.yml`. |
+| [`build_tools/generate_manifest_diff_report.py`](../../build_tools/generate_manifest_diff_report.py) | Resolves start/end SHAs, walks submodules, renders the HTML report. |
+| [`build_tools/github_actions/github_actions_api.py`](../../build_tools/github_actions/github_actions_api.py) | `gha_query_last_workflow_run()` shared helper used by `--find-last-run`. |
+| [`build_tools/github_actions/upload_test_report_script.py`](../../build_tools/github_actions/upload_test_report_script.py) | S3 upload + step-summary link (shared with test reports). |
+
+## Related
+
+- [`ci_overview.md`](ci_overview.md) — overall multi-arch CI architecture.
+- [`workflow_outputs.md`](workflow_outputs.md) — S3 layout used by the upload step.
+- [`github_actions_debugging.md`](github_actions_debugging.md) — debugging GitHub Actions runs.