Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
176 changes: 176 additions & 0 deletions .github/scripts/semgrep_ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python3

import argparse
import json
import subprocess
from pathlib import Path


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run Semgrep for a pull request and generate reviewer-facing output."
)
parser.add_argument("--repo", required=True, help="GitHub repository in owner/name form.")
parser.add_argument("--head-sha", required=True, help="Head commit SHA for the pull request.")
parser.add_argument("--base-sha", required=True, help="Base commit SHA for the pull request.")
parser.add_argument("--report", required=True, help="Path to write the Semgrep JSON report.")
parser.add_argument("--comment", required=True, help="Path to write the PR comment markdown.")
parser.add_argument(
"--github-output",
required=True,
help="Path to the GitHub Actions output file.",
)
parser.add_argument(
"--job-summary",
required=True,
help="Path to the GitHub Actions job summary file.",
)
parser.add_argument(
"--finding-limit",
type=int,
default=10,
help="Maximum number of findings to include in the PR comment.",
)
return parser.parse_args()


def run_semgrep(report_path: Path, base_sha: str) -> int:
command = [
"semgrep",
"scan",
"--config",
"p/default",
"--error",
"--disable-version-check",
"--baseline-commit",
base_sha,
"--json",
"--output",
str(report_path),
".",
]
return subprocess.run(command, check=False).returncode


def load_results(report_path: Path) -> list[dict]:
if not report_path.exists():
report_path.write_text('{"results":[]}\n', encoding="utf-8")
return []

try:
payload = json.loads(report_path.read_text(encoding="utf-8"))
except json.JSONDecodeError:
report_path.write_text('{"results":[]}\n', encoding="utf-8")
return []

return payload.get("results", [])


def changed_files_count(base_sha: str) -> int:
result = subprocess.run(
["git", "diff", "--name-only", f"{base_sha}...HEAD"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
return 0
return len([line for line in result.stdout.splitlines() if line.strip()])


def status_line(exit_code: int, findings: int) -> str:
if exit_code == 0:
return "no blocking findings"
if findings > 0:
return "blocking findings detected"
return f"Semgrep exited with status {exit_code}"


def finding_lines(
results: list[dict], repo: str, head_sha: str, finding_limit: int
) -> list[str]:
lines = []
for result in results[:finding_limit]:
path = result.get("path", "unknown")
start = result.get("start", {})
line = start.get("line", 1)
check_id = result.get("check_id", "semgrep")
message = result.get("extra", {}).get("message", "Semgrep finding")
url = f"https://github.com/{repo}/blob/{head_sha}/{path}#L{line}"
lines.append(f"- [`{path}:{line}`]({url}) - **{check_id}**: {message}")
return lines


def build_comment(
repo: str,
head_sha: str,
base_sha: str,
results: list[dict],
exit_code: int,
changed_files: int,
finding_limit: int,
) -> str:
findings = len(results)
lines = [
"## Semgrep Results",
"",
f"- Findings in changed files: {findings}",
f"- Changed files in pull request: {changed_files}",
f"- Baseline commit: `{base_sha}`",
f"- Status: {status_line(exit_code, findings)}",
"",
]

if findings > 0:
lines.append("### Findings")
lines.extend(finding_lines(results, repo, head_sha, finding_limit))
if findings > finding_limit:
lines.extend(
[
"",
f"_Showing first {finding_limit} findings. Full report is attached as `semgrep-results`._",
]
)
elif exit_code != 0:
lines.append("Semgrep did not return findings, but the scan exited unsuccessfully. See the workflow logs.")
else:
lines.append("No Semgrep findings were introduced in the files changed by this pull request.")

return "\n".join(lines) + "\n"


def write_github_outputs(output_path: Path, exit_code: int, findings: int) -> None:
with output_path.open("a", encoding="utf-8") as handle:
handle.write(f"exit_code={exit_code}\n")
handle.write(f"findings={findings}\n")


def main() -> int:
args = parse_args()
report_path = Path(args.report)
comment_path = Path(args.comment)

exit_code = run_semgrep(report_path, args.base_sha)
results = load_results(report_path)
changed_files = changed_files_count(args.base_sha)
findings = len(results)

comment = build_comment(
repo=args.repo,
head_sha=args.head_sha,
base_sha=args.base_sha,
results=results,
exit_code=exit_code,
changed_files=changed_files,
finding_limit=args.finding_limit,
)

comment_path.write_text(comment, encoding="utf-8")
with Path(args.job_summary).open("a", encoding="utf-8") as handle:
handle.write(comment)
write_github_outputs(Path(args.github_output), exit_code, findings)
return 0


if __name__ == "__main__":
raise SystemExit(main())
37 changes: 37 additions & 0 deletions .github/workflows/gradle-dependency-submission.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Gradle Dependency Submission

on:
push:
branches:
- main
paths:
- server/**
- .github/workflows/gradle-dependency-submission.yml

permissions:
contents: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
dependency-submission:
name: Submit Server Dependency Graph
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: temurin
java-version: '21'

- name: Submit Gradle dependency graph
uses: gradle/actions/dependency-submission@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1
with:
build-root-directory: server
dependency-graph: generate-and-submit
validate-wrappers: true
66 changes: 66 additions & 0 deletions .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Semgrep

on:
pull_request:
branches:
- main

permissions:
contents: read
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
semgrep:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
cache: pip

- name: Install Semgrep
run: python -m pip install --upgrade pip semgrep==1.156.0

- name: Run Semgrep
id: semgrep
env:
SEMGREP_SEND_METRICS: off
run: >-
python .github/scripts/semgrep_ci.py
--repo "${{ github.repository }}"
--head-sha "${{ github.event.pull_request.head.sha }}"
--base-sha "${{ github.event.pull_request.base.sha }}"
--report semgrep.json
--comment semgrep-comment.md
--github-output "$GITHUB_OUTPUT"
--job-summary "$GITHUB_STEP_SUMMARY"

- name: Comment on pull request
if: always()
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
header: semgrep
path: semgrep-comment.md
skip_unchanged: true

- name: Upload Semgrep report
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: semgrep-results
path: semgrep.json

- name: Enforce findings
if: always()
run: exit "${{ steps.semgrep.outputs.exit_code }}"