Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 156 additions & 1 deletion .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ jobs:
run: |
python - <<'PY'
import os
import json
from pathlib import PurePosixPath
import subprocess

Expand Down Expand Up @@ -101,9 +102,56 @@ jobs:
)

changed = sorted(path.as_posix() for path in paths if is_dependency_input(path))
# A lockfile-only PR needs a snapshot of its changed ecosystem,
# not a repo-wide PipReport resolution of unrelated requirements.
# Keep the full scan for mixed/unsupported inputs and main pushes.
snapshot_detector = ""
if changed and all(path in {"pyproject.toml", "uv.lock"} for path in changed):
if "uv.lock" in changed:
snapshot_detector = "UvLock"
elif changed and all(PurePosixPath(path).name in {"package.json", "package-lock.json"} for path in changed):
locks = {path for path in changed if PurePosixPath(path).name == "package-lock.json"}
if locks:
workspace_patterns = []
root_lock_packages = {}
root_valid = True
if "package-lock.json" in locks:
def head_json(path):
content = subprocess.run(
["git", "show", f"{head}:{path}"],
check=True, capture_output=True,
).stdout
return json.loads(content)

try:
root = head_json("package.json")
workspaces = root.get("workspaces", [])
workspace_patterns = workspaces.get("packages", []) if isinstance(workspaces, dict) else workspaces
root_lock_packages = head_json("package-lock.json").get("packages", {})
root_valid = isinstance(workspace_patterns, list) and isinstance(root_lock_packages, dict)
except (subprocess.CalledProcessError, json.JSONDecodeError, AttributeError):
root_valid = False

def covered(manifest):
path = PurePosixPath(manifest)
if path.with_name("package-lock.json").as_posix() in locks:
return True
directory = path.parent.as_posix()
return (
"package-lock.json" in locks
and directory in root_lock_packages
and any(PurePosixPath(directory).match(pattern) for pattern in workspace_patterns)
)

if root_valid and all(covered(path) for path in changed if PurePosixPath(path).name == "package.json"):
snapshot_detector = "NpmLockfile3"
elif changed == [".github/workflows/dependency-review.yml"]:
# Exercise both focused detectors on the workflow change itself.
snapshot_detector = "UvLock,NpmLockfile3"
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle:
handle.write(f"changed={'true' if changed else 'false'}\n")
handle.write(f"count={len(changed)}\n")
handle.write(f"snapshot_detector={snapshot_detector}\n")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as handle:
if changed:
handle.write("Dependency inputs changed:\n\n")
Expand All @@ -115,11 +163,118 @@ jobs:
)
PY

- name: Stage focused dependency inputs
if: >-
${{ github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
steps.dependency-changes.outputs.snapshot_detector != '' }}
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
SNAPSHOT_DETECTOR: ${{ steps.dependency-changes.outputs.snapshot_detector }}
SNAPSHOT_BASE_SOURCE: ${{ runner.temp }}/dependency-review-base
SNAPSHOT_HEAD_SOURCE: ${{ runner.temp }}/dependency-review-head
run: |
python - <<'PY'
import os
import json
from pathlib import Path
import subprocess

detector = os.environ["SNAPSHOT_DETECTOR"]
if detector == "UvLock":
locks = [Path("uv.lock")]
elif detector == "NpmLockfile3":
delta = subprocess.run(
["git", "diff", "--name-only", "-z", os.environ["BASE_SHA"], os.environ["HEAD_SHA"]],
check=True, capture_output=True,
).stdout
locks = [Path(raw.decode("utf-8")) for raw in delta.split(b"\0") if raw
and Path(raw.decode("utf-8")).name == "package-lock.json"]
else:
# The workflow-only PR exercises both lockfile ecosystems.
locks = {Path("uv.lock")}
for sha in (os.environ["BASE_SHA"], os.environ["HEAD_SHA"]):
tree = subprocess.run(
["git", "ls-tree", "-r", "--name-only", "-z", sha],
check=True, capture_output=True,
).stdout
locks.update(Path(raw.decode("utf-8")) for raw in tree.split(b"\0") if raw
and Path(raw.decode("utf-8")).name == "package-lock.json")

for sha, directory in (
(os.environ["BASE_SHA"], os.environ["SNAPSHOT_BASE_SOURCE"]),
(os.environ["HEAD_SHA"], os.environ["SNAPSHOT_HEAD_SOURCE"]),
):
target = Path(directory)
target.mkdir(parents=True, exist_ok=True)
for lock in locks:
manifest = lock.with_name("pyproject.toml" if lock.name == "uv.lock" else "package.json")
sources = {lock, manifest}
if lock == Path("package-lock.json"):
root_result = subprocess.run(
["git", "show", f"{sha}:package.json"],
capture_output=True,
)
lock_result = subprocess.run(
["git", "show", f"{sha}:package-lock.json"],
capture_output=True,
)
if root_result.returncode == 0 and lock_result.returncode == 0:
root = json.loads(root_result.stdout)
workspaces = root.get("workspaces", [])
patterns = workspaces.get("packages", []) if isinstance(workspaces, dict) else workspaces
lock_data = json.loads(lock_result.stdout)
for name in lock_data.get("packages", {}):
if name and any(Path(name).match(pattern) for pattern in patterns):
sources.add(Path(name) / "package.json")
for source in sources:
result = subprocess.run(
["git", "show", f"{sha}:{source.as_posix()}"], capture_output=True,
)
if result.returncode == 0:
destination = target / source
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(result.stdout)
PY

- name: Submit focused base dependency snapshot
if: >-
${{ github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
steps.dependency-changes.outputs.snapshot_detector != '' }}
uses: advanced-security/component-detection-dependency-submission-action@b282c67b1008fde9b60da4bdfcd8849613e293b2 # v0.1.6
with:
snapshot-sha: ${{ github.event.pull_request.base.sha }}
snapshot-ref: ${{ format('refs/heads/{0}', github.event.pull_request.base.ref) }}
filePath: ${{ runner.temp }}/dependency-review-base
correlator: focused-lockfile-review
detectorArgs: UvLock=EnableIfDefaultOff,NpmLockfile3=EnableIfDefaultOff

- name: Submit focused dependency snapshot
if: >-
${{ github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository &&
steps.dependency-changes.outputs.snapshot_detector != '' }}
uses: advanced-security/component-detection-dependency-submission-action@b282c67b1008fde9b60da4bdfcd8849613e293b2 # v0.1.6
with:
snapshot-sha: ${{ github.event.pull_request.head.sha }}
snapshot-ref: ${{ format('refs/heads/{0}', github.event.pull_request.head.ref) }}
filePath: ${{ runner.temp }}/dependency-review-head
correlator: focused-lockfile-review
detectorArgs: UvLock=EnableIfDefaultOff,NpmLockfile3=EnableIfDefaultOff
Comment thread
groupthinking marked this conversation as resolved.
Resolved

- name: Submit dependency snapshots
if: >-
${{ github.event_name == 'push' ||
(github.event.pull_request.head.repo.full_name == github.repository &&
steps.dependency-changes.outputs.changed == 'true') }}
steps.dependency-changes.outputs.changed == 'true' &&
steps.dependency-changes.outputs.snapshot_detector == '') }}
# Keep scanning the full tree, but bound each pip report. On a report
# timeout component-detection falls back to declared source dependencies.
env:
PipReportFileLevelTimeoutSeconds: "120"
uses: advanced-security/component-detection-dependency-submission-action@b282c67b1008fde9b60da4bdfcd8849613e293b2 # v0.1.6
with:
snapshot-sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
Expand Down
Loading
Loading