From f1863cc40f78c0a8a6236c8dc4253624dec9a3ec Mon Sep 17 00:00:00 2001 From: Myeongseon Choi Date: Sat, 8 Feb 2025 16:58:00 +0900 Subject: [PATCH] change ci --- .github/scripts/code_review.py | 304 ++++++++++++++++++++++++++++++ .github/scripts/model.py | 261 +++++++++++++++++++++++++ .github/workflows/ai-review.yml | 23 --- .github/workflows/ci.yaml | 196 ++----------------- .github/workflows/code-review.yml | 36 ++++ 5 files changed, 612 insertions(+), 208 deletions(-) create mode 100644 .github/scripts/code_review.py create mode 100644 .github/scripts/model.py delete mode 100644 .github/workflows/ai-review.yml create mode 100644 .github/workflows/code-review.yml diff --git a/.github/scripts/code_review.py b/.github/scripts/code_review.py new file mode 100644 index 0000000000..bb75b9bfc6 --- /dev/null +++ b/.github/scripts/code_review.py @@ -0,0 +1,304 @@ +"""Code Reviewer for Gitea.""" + +import asyncio +import fnmatch +import json +import os +import re +from typing import Any + +import requests +import aiohttp +from model import Model + +ACCESS_TOKEN = os.getenv("ACCESS_TOKEN", "") +HEADERS = {"Authorization": f"token {ACCESS_TOKEN}"} + +GITHUB_EVENT_PATH = os.getenv("GITHUB_EVENT_PATH") +try: + with open(GITHUB_EVENT_PATH, "r") as f: + EVENT_DATA = json.load(f) +except FileNotFoundError: + print("Failed to load event data.") + exit(1) + +FULL_CONTEXT_MODEL_NAME = os.getenv("FULL_CONTEXT_MODEL", "") +SINGLE_CHUNK_MODEL_NAME = os.getenv("SINGLE_CHUNK_MODEL", "") +FULL_CONTEXT_API_KEY = os.getenv("FULL_CONTEXT_API_KEY", "") +SINGLE_CHUNK_API_KEY = os.getenv("SINGLE_CHUNK_API_KEY", "") + +EXCLUDE_PATTERNS = os.getenv("EXCLUDE", "").split(",") + + +def get_diff() -> str | None: + """Get code difference between base and head from Gitea. + + Returns: + str | None: code difference between base and head, or None if failed to get diff + """ + url = EVENT_DATA["pull_request"]["diff_url"] + try: + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + return response.text + except requests.RequestException as e: + print(f"Failed to get diff: {e}") + return None + + +def parse_diff(diff: str) -> list[dict[str, Any]]: + """Parse diff into list of dicts. + + Args: + diff: str, code difference between base and head + + Returns: + list[dict[str, Any]]: list of dicts, each dict represents a code chunks + """ + file_pattern = re.compile( + r"(?s)diff --git a/(.+?) b/(.*?)\r?\n(.*?)(?=diff --git a/|$)", re.S + ) + old_new_pattern = re.compile(r"(?m)^(---|\+\+\+)\s+(.*)$") + chunk_range_pattern = re.compile( + r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*?)?(?=@@|\Z)", + re.MULTILINE | re.DOTALL, + ) + list_diff = [] + for match in file_pattern.finditer(diff): + diff_text = match.group(3) + + old_new_match = list(old_new_pattern.finditer(diff_text)) + if len(old_new_match) != 2: + continue + + old_file = old_new_match[0].group(2) + old_file = old_file.lstrip("a/") if old_file.startswith("a/") else old_file + + new_file = old_new_match[1].group(2) + if new_file == "/dev/null": + print("Neglict deleted file") + continue + new_file = new_file.lstrip("b/") + if any(fnmatch.fnmatch(new_file, pattern) for pattern in EXCLUDE_PATTERNS): + print(f"Exclude file {new_file}") + continue + + output_diff_text = [] + for chunk_range_match in chunk_range_pattern.finditer(diff_text): + old_idx = int(chunk_range_match.group(1)) + new_idx = int(chunk_range_match.group(3)) + for line in chunk_range_match.group(5).splitlines(): + if line.startswith("-"): + output_diff_text.append(f"{old_idx} None {line}") + old_idx += 1 + elif line.startswith("+"): + output_diff_text.append(f"None {new_idx} {line}") + new_idx += 1 + else: + output_diff_text.append(f"{old_idx} {new_idx} {line}") + old_idx += 1 + new_idx += 1 + + output_diff_text = "\n".join(output_diff_text) + list_diff.append( + { + "file": new_file, + "chunk": output_diff_text, + } + ) + return list_diff + + +def create_comment( + file: str, ai_response: list[dict[str, Any]] +) -> list[dict[str, Any]]: + """Create comments for single chunk review. + + Args: + file: str, file name + ai_response: list[dict[str, Any]], AI response for single chunk review + + Returns: + list[dict[str, Any]]: comments for single chunk review + """ + comments = [] + for ai_response in ai_response: + comments.append( + { + "body": f"[REVIEW] {ai_response['reviewComment']}", + "path": file, + "new_position": int(ai_response["lineNumber"]), + } + ) + return comments + + +async def analyze_single_chunks( + single_chunk_model: Model, parsed_diff: list[dict[str, Any]] +) -> list[dict[str, Any]]: + """Analyze single chunks and create comments. + + Args: + single_chunk_model: AI Session for single chunk analysis + parsed_diff: list[dict[str, Any]], parsed diff + + Returns: + list[dict[str, Any]]: comments for single chunk review + """ + + async def process_single_chunk(diff: dict[str, Any]): + file = diff["file"] + chunk = diff["chunk"] + response = await single_chunk_model.get_response_single_chunk( + file, title, description, chunk + ) + response = response.strip("`").lstrip("json").strip() or "[]" + + try: + response_json = json.loads(response) + return create_comment(file, response_json) + except json.JSONDecodeError: + print(f"Failed to parse response: {response}") + return [] + + title = EVENT_DATA["pull_request"]["title"] + description = EVENT_DATA["pull_request"]["body"] + tasks = [process_single_chunk(diff) for diff in parsed_diff] + results = await asyncio.gather(*tasks) + + # Flatten the list of comments + comments = [comment for result in results for comment in result] + return comments + + +async def get_file_content(file: str) -> str | None: + """Get file content from Gitea. + + Args: + file: str, file name + + Returns: + str | None: file content, or None if failed to get file content + """ + repo_url = EVENT_DATA["pull_request"]["head"]["repo"]["url"] + branch = EVENT_DATA["pull_request"]["head"]["ref"] + + replaced_file = file.replace("/", "%2F") + url = f"{repo_url}/raw/{branch}%2F{replaced_file}?ref={branch}" + + try: + async with aiohttp.ClientSession(headers=HEADERS) as session: + async with session.get(url) as response: + response.raise_for_status() + return await response.text() + except aiohttp.ClientError as e: # More specific exception handling + print(f"Network error fetching {file}: {e}") + except asyncio.TimeoutError: + print(f"Timeout fetching {file}") + return None + + +async def analyze_full_context( + full_context_model: Model, parsed_diff: list[dict[str, Any]] +) -> str: + """Analyze full context and create review. + + Args: + full_context_model: AI Session for full context analysis + parsed_diff: list[dict[str, Any]], parsed diff + + Returns: + str: review for full context + """ + + async def get_file_data(diff: dict[str, Any]): + file = diff["file"] + chunk = diff["chunk"] + content = get_file_content(file) + if content is None: + return None + return f"File: {file}\n{content}\nDiff: {chunk}" + + tasks = [get_file_data(diff) for diff in parsed_diff] + file_contents_list = await asyncio.gather(*tasks) + + file_contents = [item for item in file_contents_list if item is not None] + + if not file_contents: + return "" + + title = EVENT_DATA["pull_request"]["title"] + description = EVENT_DATA["pull_request"]["body"] + response = await full_context_model.get_response_full_context( + title, description, file_contents + ) + response = response.strip("`").lstrip("markdown").strip() + return response + + +def post_review( + full_context_review: str, single_chunk_comments: list[dict[str, Any]] +) -> None: + """Post review to Gitea. + + Args: + full_context_review: str, review for full context + single_chunk_comments: list[dict[str, Any]], comments for single chunk review + """ + repo_url = EVENT_DATA["pull_request"]["head"]["repo"]["url"] + pull_number = EVENT_DATA["number"] + commit_id = EVENT_DATA["pull_request"]["head"]["sha"] + url = f"{repo_url}/pulls/{pull_number}/reviews" + data = { + "body": full_context_review, + "event": "COMMENT", + "comments": single_chunk_comments, + "commit_id": commit_id, + } + response = requests.post(url, headers=HEADERS, json=data) + response.raise_for_status() + + +async def main() -> None: + """Code Reviewer for Gitea: Asynchronous version.""" + if EVENT_DATA["action"] not in ["opened", "synchronized"]: + print("Unsupported event.") + return + + diff = get_diff() + if diff is None: + return + elif not diff: + print("No diff found.") + return + + full_context_model = Model( + model=FULL_CONTEXT_MODEL_NAME, + api_key=FULL_CONTEXT_API_KEY, + is_full_context=True, + ) + single_chunk_model = Model( + model=SINGLE_CHUNK_MODEL_NAME, + api_key=SINGLE_CHUNK_API_KEY, + is_full_context=False, + ) + + parsed_diff = parse_diff(diff) + comments_task = asyncio.create_task( + analyze_single_chunks(single_chunk_model, parsed_diff) + ) + + if EVENT_DATA["action"] == "opened": + full_context_response_task = asyncio.create_task( + analyze_full_context(full_context_model, parsed_diff) + ) + full_context_response = await full_context_response_task + else: + full_context_response = "" + + comments = await comments_task + post_review(full_context_response, comments) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/scripts/model.py b/.github/scripts/model.py new file mode 100644 index 0000000000..b00ede162f --- /dev/null +++ b/.github/scripts/model.py @@ -0,0 +1,261 @@ +"""Model for code review.""" + +from enum import Enum +from typing import Any + +import google.generativeai as genai +import typing_extensions as typing +from anthropic import AsyncAnthropic +from openai import AsyncOpenAI + + +class GoogleResponse(typing.TypedDict): + """The response from Google model.""" + + lineNumber: int + reviewComment: str + + +class ModelProvider(Enum): + """The model provider.""" + + OPENAI = "openai" + ANTHROPIC = "anthropic" + GOOGLE = "google" + DEEPSEEK = "deepseek" + + @classmethod + def from_model(cls, model: str) -> "ModelProvider": + """Get the model provider from the model name. + + Args: + model (str): The model name. + + Returns: + ModelProvider: The model provider. + """ + for prefix, provider in PREFIX_TO_MODEL.items(): + if model.startswith(prefix): + return provider + raise ValueError(f"Unknown model: {model}") + + +PREFIX_TO_MODEL = { + "gpt": ModelProvider.OPENAI, + "o1": ModelProvider.OPENAI, + "o3": ModelProvider.OPENAI, + "claude": ModelProvider.ANTHROPIC, + "gemini": ModelProvider.GOOGLE, + "deepseek": ModelProvider.DEEPSEEK, +} + + +class Model: + """The model class. + + Attributes: + model (str): The model name. + api_key (str): The API key. + system_prompt (str): The system prompt. + max_tokens (int): The maximum tokens. + """ + + def __init__( # noqa: D107 + self, + model: str, + api_key: str, + is_full_context: bool, + max_tokens: int = 4196, + ): + self.model = model + self.system_prompt = ( + FULL_CONTEXT_SYSTEM_PROMPT + if is_full_context + else SINGLE_CHUNK_SYSTEM_PROMPT + ) + self.max_tokens = max_tokens + self.provider = ModelProvider.from_model(model) + self.session = self.create_session(api_key) + + def create_session(self, api_key: str) -> Any: + """Create a session for the model. + + Args: + api_key (str): The API key. + + Returns: + Any: The session. + """ + match self.provider: + case ModelProvider.OPENAI: + return AsyncOpenAI(api_key=api_key) + case ModelProvider.ANTHROPIC: + return AsyncAnthropic(api_key=api_key) + case ModelProvider.GOOGLE: + genai.configure(api_key=api_key) + return genai.GenerativeModel( + model_name=self.model, system_instruction=self.system_prompt + ) + case ModelProvider.DEEPSEEK: + return AsyncOpenAI(api_key=api_key, base_url="https://api.deepseek.com") + + async def request(self, prompt: str) -> str: + """Request the model to generate a response. + + Args: + prompt (str): The prompt to generate a response for. + + Returns: + str: The generated response. + """ + match self.provider: + case ModelProvider.OPENAI | ModelProvider.DEEPSEEK: + response = await self.session.chat.completions.create( + model=self.model, + messages=[ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": prompt}, + ], + temperature=0.2, + max_tokens=self.max_tokens, + top_p=1, + frequency_penalty=0, + presence_penalty=0, + ) + return response.choices[0].message.content.strip() + case ModelProvider.ANTHROPIC: + response = await self.session.messages.create( + model=self.model, + messages=[{"role": "user", "content": prompt}], + system=[ + { + "type": "text", + "text": self.system_prompt, + "cache_control": {"type": "ephemeral"}, + } + ], + temperature=0.2, + max_tokens=self.max_tokens, + ) + return response.content[0].text.strip() + case ModelProvider.GOOGLE: + response = await self.session.generate_content_async( + prompt, + generation_config=genai.GenerationConfig( + response_mime_type="application/json", + response_schema=list[GoogleResponse], + ), + ) + return response.text.strip() + + async def get_response_single_chunk( + self, file: str, title: str, description: str, chunk: str + ) -> str: + """Get the response for a single chunk. + + Args: + file (str): The file name. + title (str): The pull request title. + description (str): The pull request description. + chunk (str): The diff chunk. + + Returns: + str: The response. + """ + prompt = SINGLE_CHUNK_USER_PROMPT.format(file, title, description, chunk) + return await self.request(prompt) + + async def get_response_full_context( + self, title: str, description: str, file_contents: list[str] + ) -> str: + """Get the response for full context. + + Args: + title (str): The pull request title. + description (str): The pull request description. + file_contents (list[str]): The file contents, diffs. + + Returns: + str: The response. + """ + try: + prompt = FULL_CONTEXT_USER_PROMPT.format( + title, description, "\n".join(file_contents) + ) + return await self.request(prompt) + except Exception as e: + print(f"Error during full context response: {e}") + print(prompt) + return None + + +SINGLE_CHUNK_SYSTEM_PROMPT = ( + "Your task is to review pull requests. Instructions:\n" + "- Provide the response in the following JSON format: " + """[{{"lineNumber": int, "reviewComment": str}}] \n""" + "- lineNumber is about the line number of the code that in new file. \n" + "- lineNumber can be found at the front of each line. \n" + "- At the first number is old line number, the second number is new line number. \n" + "- If the line starts with `+`, it means the line is added. \n" + "- If the line starts with `-`, it means the line is deleted. \n" + "- Evaluate whether the code changes and additions are appropriate " + "and if the new code structure is suitable. \n" + "- Do not give positive comments or compliments. \n" + "- Provide comments and suggestions ONLY if there is something to improve" + "otherwise return an empty array. \n" + "- Write the comment in GitHub Markdown format. \n" + "- Use the given description only for the overall context " + "and only comment the code. \n" + "- Do not suggest type hint or naming convention. \n" + "- IMPORTANT: NEVER suggest adding comments to the code. \n" +) +SINGLE_CHUNK_USER_PROMPT = ( + "Review the following code diff in the file " + "{} and take the pull request title and description into account " + "when writing the response. \n" + "Pull request title: {} \n" + "Pull request description: \n" + "--- \n" + "{} \n" + "--- \n" + "Git diff to review: \n" + "```diff \n" + "{} \n" + "```" +) + +FULL_CONTEXT_SYSTEM_PROMPT = ( + "You are an experienced software engineer specializing in reviewing pull " + "requests. Your task is to provide an overall code review summary for a PR. " + "Focus on assessing the following aspects:\n" + "1. **Code Structure & Architecture:** " + "Evaluate whether the code is well-organized, modular, " + "and adheres to clean code principles. Suggest improvements if needed.\n" + "2. **Refactoring Opportunities:** " + "Identify areas where the code can be optimized or simplified without changing " + "its behavior.\n" + "3. **Potential Future Problems:** " + "Highlight possible scalability, maintainability, or dependency issues that might " + "arise in the future based on the current implementation.\n" + "Be constructive and clear in your feedback. Avoid commenting on trivial issues " + "or syntax errors—focus on high-level feedback.\n" + "Precise instructions:\n" + "- Do not give positive comments or compliments.\n" + "- Provide comments and suggestions ONLY if there is something to improve, " + "otherwise return an empty string.\n" + "- Write the comment in GitHub Markdown format.\n" + "- Do not start with 'markdown' or '```markdown'.\n" + "- IMPORTANT: Give example code block or pseudo code if you can.\n" +) + +FULL_CONTEXT_USER_PROMPT = ( + "Review the following code and take the pull request title " + "and description into account when writing the response. \n" + "Pull request title: {} \n" + "Pull request description: \n" + "--- \n" + "{} \n" + "--- \n" + "Code to review: \n" + "{}" +) diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml deleted file mode 100644 index d8a736056d..0000000000 --- a/.github/workflows/ai-review.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: PR Review - -on: - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - pull-requests: write - -jobs: - review: - runs-on: ubuntu-latest - steps: - - name: AI Code Review - uses: gitea-actions/ai-reviewer@v0.6 - with: - access-token: ${{ secrets.ACCESS_TOKEN }} - full-context-model: "gpt-4o" - full-context-api-key: ${{ secrets.OPENAI_API_KEY }} - single-chunk-model: "claude-3-5-sonnet" - single-chunk-api-key: ${{ secrets.ANTHROPIC_API_KEY }} - exclude-files: "*.md,*.yaml" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 73b5cc1cd6..0691dadaa0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,42 +1,14 @@ on: - push: - branches: [main, release] pull_request: + branches: [dev] types: [unlabeled, opened, synchronize, reopened] merge_group: workflow_dispatch: -name: CI - -# Cancel previous workflows if they are the same workflow on same ref (branch/tags) -# with the same event (push/pull_request) even they are in progress. -# This setting will help reduce the number of duplicated workflows. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} - cancel-in-progress: true +name: Dev-CI env: - CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,sqlite,ssl - # Skip additional tests on Windows. They are checked on Linux and MacOS. - # test_glob: many failing tests - # test_io: many failing tests - # test_os: many failing tests - # test_pathlib: support.rmtree() failing - # test_posixpath: OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)') - # test_venv: couple of failing tests - WINDOWS_SKIPS: >- - test_glob - test_io - test_os - test_rlcompleter - test_pathlib - test_posixpath - test_venv - # configparser: https://github.com/RustPython/RustPython/issues/4995#issuecomment-1582397417 - # socketserver: seems related to configparser crash. - MACOS_SKIPS: >- - test_configparser - test_socketserver + CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,sqlite,ssl # PLATFORM_INDEPENDENT_TESTS are tests that do not depend on the underlying OS. They are currently # only run on Linux to speed up the CI. PLATFORM_INDEPENDENT_TESTS: >- @@ -117,11 +89,7 @@ jobs: env: RUST_BACKTRACE: full name: Run rust tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - fail-fast: false + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -129,26 +97,12 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - - name: Set up the Windows environment - shell: bash - run: | - cargo install --target-dir=target -v cargo-vcpkg - cargo vcpkg -v build - if: runner.os == 'Windows' - - name: Set up the Mac environment - run: brew install autoconf automake libtool - if: runner.os == 'macOS' - - name: run clippy run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --exclude rustpython_wasm -- -Dwarnings - name: run rust tests run: cargo test --workspace --exclude rustpython_wasm --verbose --features threading ${{ env.CARGO_ARGS }} - if: runner.os != 'macOS' - - name: run rust tests - run: cargo test --workspace --exclude rustpython_wasm --exclude rustpython-jit --verbose --features threading ${{ env.CARGO_ARGS }} - if: runner.os == 'macOS' - + - name: check compilation without threading run: cargo check ${{ env.CARGO_ARGS }} @@ -156,24 +110,6 @@ jobs: run: cargo run --manifest-path example_projects/barebone/Cargo.toml cargo run --manifest-path example_projects/frozen_stdlib/Cargo.toml - if: runner.os == 'Linux' - - - name: prepare AppleSilicon build - uses: dtolnay/rust-toolchain@stable - with: - target: aarch64-apple-darwin - if: runner.os == 'macOS' - - name: Check compilation for Apple Silicon - run: cargo check --target aarch64-apple-darwin - if: runner.os == 'macOS' - - name: prepare iOS build - uses: dtolnay/rust-toolchain@stable - with: - target: aarch64-apple-ios - if: runner.os == 'macOS' - - name: Check compilation for iOS - run: cargo check --target aarch64-apple-ios - if: runner.os == 'macOS' exotic_targets: if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} @@ -240,11 +176,7 @@ jobs: env: RUST_BACKTRACE: full name: Run snippets and cpython tests - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - fail-fast: false + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -252,54 +184,29 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - - name: Set up the Windows environment - shell: bash - run: | - cargo install cargo-vcpkg - cargo vcpkg build - if: runner.os == 'Windows' - - name: Set up the Mac environment - run: brew install autoconf automake libtool openssl@3 - if: runner.os == 'macOS' - - name: build rustpython - run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }} - if: runner.os == 'macOS' - name: build rustpython run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }},jit - if: runner.os != 'macOS' - uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: run snippets run: python -m pip install -r requirements.txt && pytest -v working-directory: ./extra_tests - - if: runner.os == 'Linux' - name: run cpython platform-independent tests + - name: run cpython platform-independent tests run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v ${{ env.PLATFORM_INDEPENDENT_TESTS }} - - if: runner.os == 'Linux' - name: run cpython platform-dependent tests (Linux) + - name: run cpython platform-dependent tests (Linux) run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} - - if: runner.os == 'macOS' - name: run cpython platform-dependent tests (MacOS) - run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.MACOS_SKIPS }} - - if: runner.os == 'Windows' - name: run cpython platform-dependent tests (windows partial - fixme) - run: - target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.WINDOWS_SKIPS }} - - if: runner.os != 'Windows' - name: check that --install-pip succeeds + - name: check that --install-pip succeeds run: | mkdir site-packages target/release/rustpython --install-pip ensurepip --user target/release/rustpython -m pip install six - - if: runner.os != 'Windows' - name: Check that ensurepip succeeds. + - name: Check that ensurepip succeeds. run: | target/release/rustpython -m ensurepip target/release/rustpython -c "import pip" - - if: runner.os != 'Windows' - name: Check if pip inside venv is functional + - name: Check if pip inside venv is functional run: | target/release/rustpython -m venv testvenv testvenv/bin/rustpython -m pip install wheel @@ -348,84 +255,3 @@ jobs: # a memory leak, at least until we have proper cyclic gc run: MIRIFLAGS='-Zmiri-ignore-leaks' cargo +nightly miri test -p rustpython-vm -- miri_test - wasm: - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} - name: Check the WASM package and demo - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - - uses: Swatinem/rust-cache@v2 - - name: install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - name: install geckodriver - run: | - wget https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz - mkdir geckodriver - tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver - - uses: actions/setup-python@v5 - with: - python-version: ${{ env.PYTHON_VERSION }} - - run: python -m pip install -r requirements.txt - working-directory: ./wasm/tests - - uses: actions/setup-node@v4 - with: - cache: "npm" - cache-dependency-path: "wasm/demo/package-lock.json" - - name: run test - run: | - export PATH=$PATH:`pwd`/../../geckodriver - npm install - npm run test - env: - NODE_OPTIONS: "--openssl-legacy-provider" - working-directory: ./wasm/demo - - uses: mwilliamson/setup-wabt-action@v3 - with: { wabt-version: "1.0.36" } - - name: check wasm32-unknown without js - run: | - cd wasm/wasm-unknown-test - cargo build --release --verbose - if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then - echo "ERROR: wasm32-unknown module expects imports from the host environment" >2 - fi - - name: build notebook demo - if: github.ref == 'refs/heads/release' - run: | - npm install - npm run dist - mv dist ../demo/dist/notebook - env: - NODE_OPTIONS: "--openssl-legacy-provider" - working-directory: ./wasm/notebook - - name: Deploy demo to Github Pages - if: success() && github.ref == 'refs/heads/release' - uses: peaceiris/actions-gh-pages@v4 - env: - ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }} - PUBLISH_DIR: ./wasm/demo/dist - EXTERNAL_REPOSITORY: RustPython/demo - PUBLISH_BRANCH: master - - wasm-wasi: - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }} - name: Run snippets and cpython tests on wasm-wasi - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - target: wasm32-wasip1 - - - uses: Swatinem/rust-cache@v2 - - name: Setup Wasmer - uses: wasmerio/setup-wasmer@v3 - - name: Install clang - run: sudo apt-get update && sudo apt-get install clang -y - - name: build rustpython - run: cargo build --release --target wasm32-wasip1 --features freeze-stdlib,stdlib --verbose - - name: run snippets - run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/extra_tests/snippets/stdlib_random.py - - name: run cpython unittest - run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/Lib/test/test_int.py diff --git a/.github/workflows/code-review.yml b/.github/workflows/code-review.yml new file mode 100644 index 0000000000..088d5e9aca --- /dev/null +++ b/.github/workflows/code-review.yml @@ -0,0 +1,36 @@ +name: Code Review + +on: + pull_request: + branches: [dev] + types: [opened, synchronize] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install aiohttp requests py-gitea openai anthropic google-generativeai + + - name: Run Code Review + env: + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + FULL_CONTEXT_MODEL: o3-mini + FULL_CONTEXT_API_KEY: ${{ secrets.OPENAI_API_KEY }} + SINGLE_CHUNK_MODEL: gemini-2.0-flash-exp + SINGLE_CHUNK_API_KEY: ${{ secrets.GOOGLE_API_KEY }} + EXCLUDE: "*.yml,*.yaml" + run: python .gitea/scripts/code_review.py +