-
Notifications
You must be signed in to change notification settings - Fork 4
PR for #140 Automating PR reviews using GitHub Actions #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zhizhongpu
wants to merge
19
commits into
main
Choose a base branch
from
140-automating-pr-reviews-using-github-actions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
7bb86f1
#140 bd migrate in github actions
zhizhongpu 0732415
Adds datastore to workflow for #140
jmshapir d4fc0d9
Revisions to message for #140
jmshapir 6003fe3
Documents tests for #140
jmshapir 90ccf63
#140 bd extra line
zhizhongpu df96e64
#140 rename README
zhizhongpu 79ee7b9
#140 reorg rename github_action_checks.md
zhizhongpu 46e8320
Improve how we add actions [run-actions-all] #141
liaochris bf0edef
reorganize folder structure #141
liaochris 76cd84d
new url #141
liaochris 729443d
remove footer i dislike #141
liaochris f39e9c0
improve code quality #141
liaochris 3a55df2
minor improvements [run-actions-all] #141
liaochris 02b2127
more swe fixes #141 [run-actions-all]
liaochris 523f618
remove node js warnings #141 [run-actions-all]
liaochris 9f81d0b
Rename for #141
jmshapir 7f63852
Readme for #141
jmshapir 847de72
Clarifies docs for #141
jmshapir c44ac14
[run-actions-all] #141 improve style, remove eps output check
liaochris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| name: Timed Python Check | ||
| inputs: | ||
| script: | ||
| required: true | ||
| display: | ||
| required: true | ||
| runs: | ||
| using: composite | ||
| steps: | ||
| - shell: bash | ||
| run: | | ||
| mkdir -p "$RUNNER_TEMP/check_results" | ||
| s=$(date +%s%N) | ||
| python "${{ inputs.script }}" && outcome=success || outcome=failure | ||
| time=$(awk "BEGIN{printf \"%.3f\",($(date +%s%N)-$s)/1e9}") | ||
| printf '{"outcome":"%s","time":"%s"}' "$outcome" "$time" \ | ||
| > "$RUNNER_TEMP/check_results/${{ inputs.display }}.json" | ||
| [ "$outcome" = "success" ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| #!/usr/bin/env python3 | ||
| import os | ||
| import re | ||
| import sys | ||
|
|
||
| def IsExcludedPath(path, excluded): | ||
| norm = os.path.normpath(path) | ||
| return any(norm == e or norm.startswith(e + os.sep) for e in excluded) | ||
|
|
||
| def IsHidden(name): | ||
| return name.startswith(".") | ||
|
|
||
| def IsIgnoredDir(name): | ||
| return IsHidden(name) or name == "__pycache__" | ||
|
|
||
| def ReadFile(path): | ||
| try: | ||
| with open(path, "r", encoding="utf-8") as fh: | ||
| return fh.read() | ||
| except Exception: | ||
| return None | ||
|
|
||
| def WalkFiles(root, excluded, extension): | ||
| for dir_path, dir_names, file_names in os.walk(root): | ||
| if IsExcludedPath(dir_path, excluded): | ||
| dir_names[:] = [] | ||
| continue | ||
|
|
||
| dir_names[:] = [d for d in dir_names if not IsIgnoredDir(d)] | ||
|
|
||
| for file_name in file_names: | ||
| if file_name.endswith(extension) and not IsHidden(file_name): | ||
| yield os.path.join(dir_path, file_name) | ||
|
|
||
| def CheckEpsSavefig(content): | ||
| eps_savefig_patterns = [ | ||
| r'\.savefig\([^)]*[\'"].*eps.*[\'"][^)]*\)', | ||
| r'\.savefig\([^)]*format\s*=\s*[\'"]eps[\'"][^)]*\)' | ||
| ] | ||
|
|
||
| eps_lines = [] | ||
| for line_num, line in enumerate(content.split('\n'), 1): | ||
| for pattern in eps_savefig_patterns: | ||
| if re.search(pattern, line, re.IGNORECASE): | ||
| eps_lines.append(f"Line {line_num}: {line.strip()}") | ||
| break | ||
|
|
||
| remove_count = content.count('remove_eps_info(') | ||
| return eps_lines if len(eps_lines) != remove_count else [] | ||
|
|
||
| def CollectEpsProblems(root, excluded): | ||
| problems = [] | ||
| for file_path in WalkFiles(root, excluded, '.py'): | ||
| content = ReadFile(file_path) | ||
| if content is None: | ||
| continue | ||
| file_problems = CheckEpsSavefig(content) | ||
| if file_problems: | ||
| problems.append({'file': file_path, 'issues': file_problems}) | ||
| return problems | ||
|
|
||
| def Main(): | ||
| source_root = "source" | ||
| excluded = ["source/lib", "source/raw", "source/scrape"] | ||
|
|
||
| problems = CollectEpsProblems(source_root, excluded) | ||
|
|
||
| if problems: | ||
| print("EPS savefig check failed!") | ||
| print("\nPython files using .savefig(*eps*) without remove_eps_info():") | ||
| for problem in problems: | ||
| print(f"\nFile: {problem['file']}") | ||
| for issue in problem['issues']: | ||
| print(f" {issue}") | ||
| print("\nTo fix: Add 'from source.lib.JMSLab.remove_eps_info import remove_eps_info'") | ||
| print("and call 'remove_eps_info(filename)' after each EPS savefig.") | ||
| return 1 | ||
| else: | ||
| print("EPS savefig check: all checks passed.") | ||
| return 0 | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(Main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| #!/usr/bin/env python3 | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| EXCLUDED_EXT = {".log", ".txt"} | ||
|
|
||
| INCLUDED_DIRS = {"source", "output"} | ||
|
|
||
| def GetTrackedFiles(): | ||
| return [Path(p) for p in subprocess.run(["git","ls-files"], stdout=subprocess.PIPE, check=True).stdout.decode().splitlines() if p.strip() and Path(p).parts[0] in INCLUDED_DIRS] | ||
|
|
||
| def IsBinary(p): | ||
| try: | ||
| return b"\0" in p.read_bytes()[:4096] | ||
| except Exception: | ||
| return True | ||
|
|
||
| def NeedsNewline(p): | ||
| try: | ||
| d = p.read_bytes() | ||
| return len(d)==0 or not d.endswith(b"\n") | ||
| except Exception: | ||
| return False | ||
|
|
||
| def ProcessFiles(): | ||
| missing = [p for p in GetTrackedFiles() if p.exists() and p.suffix not in EXCLUDED_EXT and not IsBinary(p) and NeedsNewline(p)] | ||
| if not missing: | ||
| print("No files missing trailing newlines."); return 0 | ||
| print("Files missing trailing newline:") | ||
| for p in missing: print(" -", p) | ||
| return 1 | ||
|
|
||
| def Main(): | ||
| return ProcessFiles() | ||
|
|
||
| if __name__=="__main__": | ||
| sys.exit(Main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| #!/usr/bin/env python3 | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| TARGET = "terminated because of errors." | ||
|
|
||
| def Main(): | ||
| bad = [] | ||
| for p in Path(".").rglob("*.log"): | ||
| try: | ||
| if TARGET in p.read_text(errors="replace"): | ||
| bad.append(p) | ||
| except Exception: | ||
| pass | ||
|
|
||
| if not bad: | ||
| print("No log files contain the error string.") | ||
| return 0 | ||
|
|
||
| print("Problematic log files:") | ||
| for p in bad: | ||
| print(" -", p) | ||
|
|
||
| return 1 | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(Main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| #!/usr/bin/env python3 | ||
| import os | ||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| ROOT = Path("source") | ||
| EXCLUDED = {Path("source/lib"), Path("source/raw"), Path("source/scrape")} | ||
| PAPER_DIR = Path("source/paper") | ||
| PAPER_EXTS = {".bib", ".tex", ".lyx"} | ||
|
|
||
|
|
||
| def Main(): | ||
| missing_dirs, missing_mentions = CollectProblems() | ||
| missing_in_sconstruct = SourceFoldersMissingInSConstruct() | ||
| any_problems = bool(missing_dirs or missing_mentions or missing_in_sconstruct) | ||
| if any_problems: | ||
| print("SConscript/SConstruct summary of missing items:") | ||
| if missing_dirs: | ||
| print("\nFolders missing SConscript:") | ||
| for p in missing_dirs: | ||
| print(p) | ||
| if missing_mentions: | ||
| print("\nFiles not mentioned in their SConscript:") | ||
| for p in missing_mentions: | ||
| print(p) | ||
| if missing_in_sconstruct: | ||
| print("\nTop-level source folders missing from root SConstruct:") | ||
| for p in missing_in_sconstruct: | ||
| print(p) | ||
| else: | ||
| print("SConscript/SConstruct summary: all checks passed.") | ||
| return 1 if any_problems else 0 | ||
|
|
||
|
|
||
| def CollectProblems(): | ||
| missing_dirs = [] | ||
| missing_mentions = [] | ||
| for dir_path, dir_names, file_names in os.walk(ROOT): | ||
| dir_path = Path(dir_path) | ||
| if IsExcluded(dir_path): | ||
| dir_names[:] = [] | ||
| continue | ||
| dir_names[:] = sorted(d for d in dir_names if not IsIgnored(d)) | ||
| file_names = [f for f in file_names if not IsIgnored(f)] | ||
| if dir_path == ROOT or (not dir_names and not file_names): | ||
| continue | ||
| content = Read(dir_path / "SConscript") | ||
| if content is None: | ||
| parent_content = Read(dir_path.parent / "SConscript") | ||
| if parent_content is None: | ||
| missing_dirs.append(dir_path) | ||
| continue | ||
| for f in sorted(f for f in file_names if f != "SConscript"): | ||
| if ShouldCheck(dir_path, f) and not IsMentioned(content, f, dir_path): | ||
| missing_mentions.append(f"{dir_path} -> {f}") | ||
| for subdir in dir_names: | ||
| if re.search(rf"\b{re.escape(subdir)}\b", content): | ||
| continue | ||
| subdir_path = dir_path / subdir | ||
| try: | ||
| subfiles = sorted( | ||
| e for e in os.listdir(subdir_path) | ||
| if not IsIgnored(e) and (subdir_path / e).is_file() and ShouldCheck(subdir_path, e) | ||
| ) | ||
| except Exception: | ||
| missing_dirs.append(subdir_path) | ||
| continue | ||
| for f in subfiles: | ||
| if not IsMentioned(content, f, subdir_path): | ||
| missing_mentions.append(f"{dir_path} -> {subdir}/{f}") | ||
| return missing_dirs, missing_mentions | ||
|
|
||
|
|
||
| def IsExcluded(dir_path): | ||
| return any(dir_path == e or dir_path.is_relative_to(e) for e in EXCLUDED) | ||
|
|
||
|
|
||
| def IsIgnored(name): | ||
| return name.startswith(".") or name == "__pycache__" | ||
|
|
||
|
|
||
| def ShouldCheck(dir_path, name): | ||
| if dir_path.is_relative_to(PAPER_DIR): | ||
| return Path(name).suffix in PAPER_EXTS | ||
| return True | ||
|
|
||
|
|
||
| def Read(path): | ||
| try: | ||
| return Path(path).read_text(encoding="utf-8") | ||
| except Exception: | ||
| return None | ||
|
|
||
|
|
||
| def IsMentioned(content, name, dir_path): | ||
| rel = dir_path.relative_to(ROOT).as_posix() | ||
| return f"#source/{rel}/{name}" in content | ||
|
|
||
|
|
||
| def SourceFoldersMissingInSConstruct(): | ||
| content = Read("SConstruct") | ||
| folders = sorted( | ||
| e for e in os.listdir(ROOT) | ||
| if (ROOT / e).is_dir() and not IsIgnored(e) and not IsExcluded(ROOT / e) | ||
| ) | ||
| if content is None: | ||
| return folders | ||
| return [f for f in folders if f"source/{f}/SConscript" not in content] | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(Main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [ | ||
| {"name": "SCons DAG", "command": "/run-actions-dag"}, | ||
| {"name": "Newlines", "command": "/run-actions-newlines"}, | ||
| {"name": "EPS data", "command": "/run-actions-eps"}, | ||
| {"name": "Build log", "command": "/run-actions-log"} | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| #!/usr/bin/env python3 | ||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| def LoadChecks(): | ||
| checks_path = Path(__file__).parent.parent / 'checks' / 'checks.json' | ||
| return json.loads(checks_path.read_text()) | ||
|
|
||
| def ParseCommands(): | ||
| checks = LoadChecks() | ||
| comment = os.environ.get('COMMENT_BODY', '') | ||
| return [c['name'] for c in checks | ||
| if os.environ.get('GITHUB_EVENT_NAME') == 'push' | ||
| or '/run-actions-all' in comment | ||
| or c.get('command', '') in comment] | ||
|
|
||
| def Main(): | ||
| print(json.dumps(ParseCommands())) | ||
| return 0 | ||
|
|
||
| if __name__ == '__main__': | ||
| sys.exit(Main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #!/usr/bin/env python3 | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| CHECKS_JSON = Path(__file__).parent.parent / 'checks' / 'checks.json' | ||
|
|
||
| def Main(): | ||
| repo = os.environ["GITHUB_REPOSITORY"] | ||
| run_id = os.environ["GITHUB_RUN_ID"] | ||
|
|
||
| print("Runtime:") | ||
| rows, failed = CollectResults() | ||
|
|
||
| if "--post" in sys.argv: | ||
| PostResults(repo, run_id, rows, failed) | ||
|
|
||
| if failed: | ||
| print(f"Failed checks: {', '.join(failed)}") | ||
| return 1 | ||
| return 0 | ||
|
|
||
| def CollectResults(): | ||
| checks = json.loads(CHECKS_JSON.read_text()) | ||
| results_dir = Path(os.environ["RUNNER_TEMP"]) / "check_results" | ||
| rows, failed = [], [] | ||
| STATUS = {"success": "✅", "failure": "❌"} | ||
| for check in checks: | ||
| check_name = check["name"] | ||
| result_file = results_dir / f"{check_name}.json" | ||
| if result_file.exists(): | ||
| result = json.loads(result_file.read_text()) | ||
| outcome = result["outcome"] | ||
| elapsed = result["time"] | ||
| print(f" {check_name}: {elapsed}s") | ||
| status_icon = STATUS.get(outcome, "❌") | ||
| if outcome != "success": | ||
| failed.append(check_name) | ||
| rows.append(f"| {check_name} | {status_icon} | {elapsed}s |") | ||
| else: | ||
| print(f" {check_name}: skipped") | ||
| rows.append(f"| {check_name} | SKIP | |") | ||
| return rows, failed | ||
|
|
||
| def PostResults(repo, run_id, rows, failed): | ||
| run_url = f"https://github.com/{repo}/actions/runs/{run_id}" | ||
| table = "\n".join(["| Check | Result | Time |", "|-------|--------|------|", *rows]) | ||
| body = f"**Check Results** ([run details]({run_url}))\n\n{table}" | ||
| pr_num = os.environ["PR_NUMBER"] | ||
| pr_sha = os.environ["PR_SHA"] | ||
| subprocess.run([ | ||
| "gh", "api", f"repos/{repo}/issues/{pr_num}/comments", | ||
| "--method", "POST", "-f", f"body={body}", | ||
| ], check=True) | ||
| state = "failure" if failed else "success" | ||
| desc = f"Failed: {', '.join(failed)}" if failed else "All checks passed" | ||
| subprocess.run([ | ||
| "gh", "api", f"repos/{repo}/statuses/{pr_sha}", | ||
| "-f", f"state={state}", "-f", "context=Checks", | ||
| "-f", f"description={desc}", "-f", f"target_url={run_url}", | ||
| ], check=True) | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(Main()) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.