chore: restore original directory structure (project under code-review-graph-main/)
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: pip install -e ".[dev]"
|
||||
- name: Lint with ruff
|
||||
run: ruff check code_review_graph/
|
||||
|
||||
type-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: pip install -e ".[dev]" mypy types-networkx
|
||||
- name: Run mypy
|
||||
run: mypy code_review_graph/ --ignore-missing-imports --no-strict-optional
|
||||
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install bandit
|
||||
run: pip install bandit[toml]
|
||||
- name: Run bandit security scan
|
||||
run: bandit -r code_review_graph/ -c pyproject.toml
|
||||
|
||||
schema-sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Check Python/VSCode schema versions match
|
||||
run: |
|
||||
PY_VER=$(grep -oP 'LATEST_VERSION\s*=\s*max\(MIGRATIONS\.keys\(\)\)' code_review_graph/migrations.py > /dev/null && python3 -c "
|
||||
import re, ast
|
||||
src = open('code_review_graph/migrations.py').read()
|
||||
m = re.search(r'MIGRATIONS:\s*dict\[.*?\]\s*=\s*\{([^}]+)\}', src)
|
||||
keys = [int(k.strip().rstrip(':')) for k in re.findall(r'(\d+):', m.group(1))]
|
||||
print(max(keys))
|
||||
")
|
||||
TS_VER=$(grep -oP 'SUPPORTED_SCHEMA_VERSION\s*=\s*\K\d+' code-review-graph-vscode/src/backend/sqlite.ts)
|
||||
echo "Python LATEST_VERSION: $PY_VER"
|
||||
echo "VSCode SUPPORTED_SCHEMA_VERSION: $TS_VER"
|
||||
if [ "$PY_VER" != "$TS_VER" ]; then
|
||||
echo "::error::Schema version mismatch! Python=$PY_VER, VSCode=$TS_VER"
|
||||
exit 1
|
||||
fi
|
||||
echo "Schema versions in sync."
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: pip install -e ".[dev]" pytest-cov
|
||||
- name: Run tests with coverage
|
||||
run: pytest --tb=short -q --cov=code_review_graph --cov-report=term-missing --cov-fail-under=65
|
||||
|
||||
windows-native:
|
||||
name: Windows daemon and file handles
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: pip install -e ".[dev]"
|
||||
- name: Run native daemon and resource-handle tests
|
||||
run: >-
|
||||
python -m pytest --tb=short -q
|
||||
tests/test_windows_compat.py
|
||||
tests/test_daemon.py
|
||||
tests/test_changes.py
|
||||
tests/test_communities.py
|
||||
tests/test_flows.py
|
||||
tests/test_graph.py
|
||||
tests/test_integration_v2.py
|
||||
tests/test_migrations.py
|
||||
tests/test_postprocessing.py
|
||||
tests/test_refactor.py
|
||||
tests/test_search.py
|
||||
tests/test_tools.py
|
||||
tests/test_wiki.py
|
||||
tests/test_skills.py::TestInstallCodexHooks
|
||||
tests/test_skills.py::TestInstallCursorHooks
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Weekly Eval
|
||||
|
||||
# Report-only benchmark run. This workflow surfaces benchmark drift in the
|
||||
# job summary and the uploaded CSV artifact, but it must NOT fail the default
|
||||
# branch on regressions (yet) — eval failures are informational until the
|
||||
# co-change baseline has enough history to set thresholds against.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "23 6 * * 1" # Mondays 06:23 UTC (off-minute to dodge load spikes)
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
eval:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install with eval extras
|
||||
run: pip install -e ".[eval]"
|
||||
|
||||
- name: Run benchmarks (2 smallest pinned configs)
|
||||
# httpx (~60 files) and flask (~83 files) are the two smallest
|
||||
# pinned repos. Report-only: `|| true` keeps regressions and
|
||||
# transient clone failures from failing the default branch.
|
||||
run: |
|
||||
code-review-graph eval \
|
||||
--repo httpx,flask \
|
||||
--benchmark token_efficiency,impact_accuracy,agent_baseline \
|
||||
--output-dir evaluate/results || true
|
||||
|
||||
- name: Upload result CSVs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: eval-results-${{ github.run_id }}
|
||||
path: evaluate/results/*.csv
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
|
||||
- name: Write job summary
|
||||
if: always()
|
||||
run: |
|
||||
python - <<'PY' >> "$GITHUB_STEP_SUMMARY"
|
||||
from code_review_graph.eval.reporter import generate_full_report
|
||||
|
||||
print("# Weekly eval (report-only)")
|
||||
print()
|
||||
print(
|
||||
"Configs: `httpx`, `flask` (the two smallest pinned repos). "
|
||||
"Regressions are reported here and in the CSV artifact but do "
|
||||
"not fail CI."
|
||||
)
|
||||
print()
|
||||
print(generate_full_report("evaluate/results"))
|
||||
PY
|
||||
@@ -0,0 +1,165 @@
|
||||
# Posts the report produced by the unprivileged PR Review workflow. This
|
||||
# workflow runs from the default branch, never checks out PR code, and treats
|
||||
# every downloaded artifact byte as untrusted input.
|
||||
name: PR Review Comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["PR Review"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Locate one bounded report artifact
|
||||
id: artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAX_ARCHIVE_BYTES: "100000"
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t artifact_rows < <(gh api --method GET \
|
||||
"repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts" \
|
||||
-f per_page=100 \
|
||||
--jq '.artifacts[] | select(
|
||||
.name == "crg-report" and (.expired | not)
|
||||
) | [.id, .size_in_bytes] | @tsv')
|
||||
if [ "${#artifact_rows[@]}" -ne 1 ]; then
|
||||
echo "Expected exactly one non-expired crg-report artifact." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=$'\t' read -r artifact_id artifact_size <<< "${artifact_rows[0]}"
|
||||
case "${artifact_id}" in
|
||||
''|*[!0-9]*) echo "Invalid artifact ID." >&2; exit 1 ;;
|
||||
esac
|
||||
case "${artifact_size}" in
|
||||
''|*[!0-9]*) echo "Invalid artifact size." >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "${artifact_size}" -eq 0 ] || \
|
||||
[ "${artifact_size}" -gt "${MAX_ARCHIVE_BYTES}" ]; then
|
||||
echo "Artifact archive is empty or exceeds the size cap." >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'artifact-id=%s\n' "${artifact_id}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Download report artifact into runner temp
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
artifact-ids: ${{ steps.artifact.outputs.artifact-id }}
|
||||
path: ${{ runner.temp }}/crg-report-download
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Validate and wrap untrusted report
|
||||
env:
|
||||
COMMENT_BODY: ${{ runner.temp }}/crg-comment-body.md
|
||||
DOWNLOAD_DIR: ${{ runner.temp }}/crg-report-download
|
||||
MAX_BODY_BYTES: "65000"
|
||||
MAX_PR_NUMBER_BYTES: "12"
|
||||
MAX_REPORT_BYTES: "60000"
|
||||
TRUSTED_MARKER: <!-- code-review-graph-report -->
|
||||
VALIDATED_PR_NUMBER: ${{ runner.temp }}/crg-pr-number.txt
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python - <<'PY'
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(os.environ["DOWNLOAD_DIR"])
|
||||
if not root.is_dir() or root.is_symlink():
|
||||
raise SystemExit("Artifact extraction root is not a safe directory")
|
||||
|
||||
entries = {path.name: path for path in root.iterdir()}
|
||||
expected = {"crg-comment.md", "pr-number.txt"}
|
||||
if set(entries) != expected:
|
||||
raise SystemExit("Artifact must contain exactly the expected files")
|
||||
if any(path.is_symlink() or not path.is_file() for path in entries.values()):
|
||||
raise SystemExit("Artifact entries must be regular files")
|
||||
|
||||
pr_file = entries["pr-number.txt"]
|
||||
if pr_file.stat().st_size > int(os.environ["MAX_PR_NUMBER_BYTES"]):
|
||||
raise SystemExit("PR number artifact is too large")
|
||||
try:
|
||||
pr_text = pr_file.read_bytes().decode("ascii")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise SystemExit("PR number must be ASCII") from exc
|
||||
if re.fullmatch(r"[1-9][0-9]{0,9}\n?", pr_text) is None:
|
||||
raise SystemExit("PR number must contain only a positive integer")
|
||||
|
||||
report_file = entries["crg-comment.md"]
|
||||
report_size = report_file.stat().st_size
|
||||
if report_size == 0 or report_size > int(os.environ["MAX_REPORT_BYTES"]):
|
||||
raise SystemExit("Report artifact is empty or too large")
|
||||
try:
|
||||
text = report_file.read_bytes().decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise SystemExit("Report must be valid UTF-8") from exc
|
||||
if any(ord(char) < 32 and char not in "\n\r\t" for char in text):
|
||||
raise SystemExit("Report contains disallowed control characters")
|
||||
if "\x7f" in text:
|
||||
raise SystemExit("Report contains disallowed control characters")
|
||||
|
||||
marker = os.environ["TRUSTED_MARKER"]
|
||||
text = text.replace(marker, "")
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n").lstrip("\n")
|
||||
if not text.startswith("## code-review-graph review\n"):
|
||||
raise SystemExit("Report has an unexpected heading")
|
||||
if "*Powered by [code-review-graph]" not in text:
|
||||
raise SystemExit("Report is missing its expected footer")
|
||||
|
||||
# Prevent PR-controlled report text from creating GitHub mentions.
|
||||
text = text.replace("@", "@")
|
||||
body = f"{marker}\n\n{text}"
|
||||
body_bytes = body.encode("utf-8")
|
||||
if len(body_bytes) > int(os.environ["MAX_BODY_BYTES"]):
|
||||
raise SystemExit("Wrapped comment exceeds the GitHub body limit")
|
||||
|
||||
Path(os.environ["COMMENT_BODY"]).write_bytes(body_bytes)
|
||||
Path(os.environ["VALIDATED_PR_NUMBER"]).write_text(
|
||||
str(int(pr_text)), encoding="ascii"
|
||||
)
|
||||
PY
|
||||
|
||||
- name: Upsert sticky PR comment
|
||||
env:
|
||||
COMMENT_BODY: ${{ runner.temp }}/crg-comment-body.md
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
PR_NUMBER_FILE: ${{ runner.temp }}/crg-pr-number.txt
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pr_number=$(cat "${PR_NUMBER_FILE}")
|
||||
actual_sha=$(gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/pulls/${pr_number}" --jq '.head.sha')
|
||||
if [ "${actual_sha}" != "${HEAD_SHA}" ]; then
|
||||
echo "PR head does not match the analyzed commit; refusing to comment." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
comment_id=$(gh api \
|
||||
"repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \
|
||||
--paginate \
|
||||
--jq '.[] | select(
|
||||
.user.login == "github-actions[bot]" and
|
||||
(.body | startswith("<!-- code-review-graph-report -->"))
|
||||
) | .id' | sed -n '1p')
|
||||
if [ -n "${comment_id}" ]; then
|
||||
gh api --method PATCH --silent \
|
||||
"repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" \
|
||||
-F body=@"${COMMENT_BODY}"
|
||||
else
|
||||
gh api --method POST --silent \
|
||||
"repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \
|
||||
-F body=@"${COMMENT_BODY}"
|
||||
fi
|
||||
@@ -0,0 +1,42 @@
|
||||
# Dogfoods the local composite action (action.yml at the repo root) on PRs.
|
||||
# This run is intentionally unprivileged. A separate workflow_run workflow
|
||||
# validates the rendered report and posts the sticky comment.
|
||||
name: PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Run code-review-graph review
|
||||
id: review
|
||||
uses: ./
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment: "false"
|
||||
fail-on-risk: none
|
||||
- name: Stage report artifact
|
||||
env:
|
||||
ARTIFACT_DIR: ${{ runner.temp }}/crg-report
|
||||
COMMENT_FILE: ${{ steps.review.outputs.comment-file }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf -- "${ARTIFACT_DIR}"
|
||||
mkdir -p -- "${ARTIFACT_DIR}"
|
||||
cp -- "${COMMENT_FILE}" "${ARTIFACT_DIR}/crg-comment.md"
|
||||
printf '%s\n' "${PR_NUMBER}" > "${ARTIFACT_DIR}/pr-number.txt"
|
||||
- name: Upload report artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: crg-report
|
||||
path: ${{ runner.temp }}/crg-report/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install build tools
|
||||
run: pip install build twine
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: twine upload dist/*
|
||||
Reference in New Issue
Block a user