diff --git a/.github/.codecov.yml b/.github/.codecov.yml new file mode 100644 index 00000000..c88b947e --- /dev/null +++ b/.github/.codecov.yml @@ -0,0 +1,16 @@ +ignore: + - "crates/**/*test*.rs" +coverage: + status: + project: + default: + target: auto # set the target coverage to the value of the parent commit + threshold: 0% # pct of drop in coverage that is still considered as success + informational: true # if true does not fail the CI is coverage is bellow the target value + only_pulls: true # run only on PRs + patch: + default: + target: 100% + threshold: 0% + informational: true + only_pulls: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..10caba84 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,111 @@ +name: CI + +on: + push: + branches: + - main + - main-v[0-9].** + tags: + - v[0-9].** + + pull_request: + types: + - opened + - reopened + - synchronize + - auto_merge_enabled + - edited + +jobs: + commitlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install commitlint + run: npm install --global @commitlint/cli @commitlint/config-conventional + + - name: Validate PR commits with commitlint + if: github.event_name == 'pull_request' && !(contains(github.event.pull_request.title, '/merge-main') || contains(github.event.pull_request.title, '/merge main')) + run: commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + + - name: Validate PR title with commitlint + if: github.event_name != 'merge_group' && github.event_name != 'push' && !(contains(github.event.pull_request.title, '/merge-main') || contains(github.event.pull_request.title, '/merge main')) + run: echo "${{ github.event.pull_request.title }}" | commitlint --verbose + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + components: rustfmt + toolchain: nightly-2024-01-12 + - uses: Swatinem/rust-cache@v2 + - run: scripts/rust_fmt.sh --check + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: scripts/clippy.sh + + run-python-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - run: | + python -m pip install --upgrade pip + pip install pytest + - run: pytest scripts/merge_paths_test.py + + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo test + + udeps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + name: "Rust Toolchain Setup" + with: + toolchain: nightly-2024-01-12 + - uses: Swatinem/rust-cache@v2 + id: "cache-cargo" + - if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} + name: "Download and run cargo-udeps" + run: | + wget -O - -c https://github.com/est31/cargo-udeps/releases/download/v0.1.45/cargo-udeps-v0.1.45-x86_64-unknown-linux-gnu.tar.gz | tar -xz + cargo-udeps-*/cargo-udeps udeps + env: + RUSTUP_TOOLCHAIN: nightly-2024-01-12 + + + all-tests: + runs-on: ubuntu-latest + needs: + - clippy + - commitlint + - format + - run-python-tests + - run-tests + - udeps + steps: + - name: Decide whether all the needed jobs succeeded or failed + uses: re-actors/alls-green@v1.2.2 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/compiled_cairo.yml b/.github/workflows/compiled_cairo.yml new file mode 100644 index 00000000..747c6349 --- /dev/null +++ b/.github/workflows/compiled_cairo.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: + - main + tags: + - v[0-9].** + + pull_request: + types: + - opened + - reopened + - synchronize + paths: + - 'crates/blockifier/feature_contracts/cairo0/**' + +jobs: + verify_cairo_file_dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + components: rustfmt + toolchain: nightly-2024-01-12 + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + - run: + pip install -r crates/blockifier/tests/requirements.txt; + cargo test verify_feature_contracts -- --include-ignored diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..1e2bbc89 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,25 @@ +name: Coverage + +on: [pull_request, push] + +jobs: + coverage: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Generate code coverage + run: cargo llvm-cov --codecov --output-path codecov.json + env: + SEED: 0 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + fail_ci_if_error: true diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml new file mode 100644 index 00000000..b93bc468 --- /dev/null +++ b/.github/workflows/post-merge.yml @@ -0,0 +1,24 @@ +name: post-merge + +on: + pull_request: + types: + - closed +jobs: + if_merged: + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + components: rustfmt + toolchain: nightly-2024-01-12 + - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + cache: 'pip' + - run: + pip install -r crates/blockifier/tests/requirements.txt; + cargo test -- --include-ignored diff --git a/.github/workflows/verify-deps.yml b/.github/workflows/verify-deps.yml new file mode 100644 index 00000000..3d773321 --- /dev/null +++ b/.github/workflows/verify-deps.yml @@ -0,0 +1,20 @@ +name: Nightly Latest Dependencies Check + +on: + schedule: + - cron: '0 0 * * *' # Runs at 00:00 UTC every day + +jobs: + latest_deps: + name: Latest Dependencies + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Update Dependencies + run: cargo update --verbose + - name: Build + run: cargo build --verbose + - name: Test + run: cargo test --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e21a6d42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.egg-info +/data +/logs +build +dist +target +*/.vscode/* +*.DS_Store + +tmp_venv/* + diff --git a/scripts/clippy.sh b/scripts/clippy.sh new file mode 100755 index 00000000..63cfd371 --- /dev/null +++ b/scripts/clippy.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cargo clippy "$@" --all-targets --all-features -- -D warnings -D future-incompatible \ + -D nonstandard-style -D rust-2018-idioms -D unused diff --git a/scripts/merge_branches.py b/scripts/merge_branches.py new file mode 100755 index 00000000..32e264b8 --- /dev/null +++ b/scripts/merge_branches.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3.9 + +""" +Merge a branch into another branch. Example usage: +``` +scripts/merge_branches.py --src main-v0.13.0 +``` +""" + +import argparse +import json +import os +import subprocess +from typing import Dict, List, Optional + +FINAL_BRANCH = "main" +MERGE_PATHS_FILE = "scripts/merge_paths.json" + + +def load_merge_paths() -> Dict[str, str]: + return json.load(open(MERGE_PATHS_FILE)) + + +def run_command(command: str, allow_error: bool = False) -> List[str]: + """ + Runs a bash command and returns the output as a list of lines. + """ + try: + command_output = ( + subprocess.check_output(command, shell=True, cwd=os.getcwd()) + .decode("utf-8") + .splitlines() + ) + output_lines = "\n".join(command_output) + print(f"Command '{command}' output:\n{output_lines}") + return command_output + except subprocess.CalledProcessError as error: + if not allow_error: + raise + print(f"Command '{command}' hit error: {error=}.") + return str(error).splitlines() + + +def get_dst_branch(src_branch: str, dst_branch_override: Optional[str]) -> str: + if dst_branch_override is not None: + return dst_branch_override + assert ( + src_branch.replace("origin/", "") != FINAL_BRANCH + ), f"{FINAL_BRANCH} has no default destination branch." + + return load_merge_paths()[src_branch] + + +def srcdiff(source_branch: str, destination_branch: Optional[str], files: List[str]): + destination_branch = get_dst_branch( + src_branch=source_branch, dst_branch_override=destination_branch + ) + files_line = " ".join(files) + run_command( + f"git diff $(git merge-base origin/{source_branch} origin/{destination_branch}) " + f"origin/{source_branch} {files_line}" + ) + + +def dstdiff(source_branch: str, destination_branch: Optional[str], files: List[str]): + destination_branch = get_dst_branch( + src_branch=source_branch, dst_branch_override=destination_branch + ) + files_line = " ".join(files) + run_command( + f"git diff $(git merge-base origin/{source_branch} origin/{destination_branch}) " + f"origin/{destination_branch} {files_line}" + ) + + +def merge_branches(src_branch: str, dst_branch: Optional[str]): + """ + Merge source branch into destination branch. + If no destination branch is passed, the destination branch is taken from state on repo. + """ + user = os.environ["USER"] + dst_branch = get_dst_branch(src_branch=src_branch, dst_branch_override=dst_branch) + + merge_branch = f"{user}/merge-{src_branch}-into-{dst_branch}" + print(f"Source branch: {src_branch}") + print(f"Destination branch: {dst_branch}\n") + + run_command("git fetch") + run_command(f"git checkout origin/{dst_branch}") + run_command(f"git checkout -b {merge_branch}") + print("Merging...") + run_command("git config merge.conflictstyle diff3") + + run_command(f"git merge origin/{src_branch}", allow_error=True) + + run_command("git config --unset merge.conflictstyle") + run_command("git status -s | grep \"^UU\" | awk '{ print $2 }' | tee /tmp/conflicts") + + conflicts_file = "/tmp/conflicts" + conflicts = [line.strip() for line in open(conflicts_file).readlines() if line.strip() != ""] + conflict_line = " ".join(conflicts) + run_command(f"git add {conflict_line}", allow_error=True) + run_command("git add changed_files/*", allow_error=True) + print("Committing conflicts...") + if len(conflicts) == 0: + run_command( + f'git commit --allow-empty -m "No conflicts in {src_branch} -> {dst_branch} merge, ' + 'this commit is for any change needed to pass the CI."' + ) + else: + run_command(f'git commit -m "chore: merge branch {src_branch} into {dst_branch} (with conflicts)"') + + print("Pushing...") + run_command(f"git push --set-upstream origin {merge_branch}") + (merge_base,) = run_command(f"git merge-base origin/{src_branch} origin/{dst_branch}") + + print("Creating PR...") + run_command( + f'gh pr create --base {dst_branch} --title "Merge {src_branch} into {dst_branch}" ' + '--body ""' + ) + + if len(conflicts) != 0: + compare = "https://github.com/starkware-libs/blockifier/compare" + comment_file_path = "/tmp/comment.XXXXXX" + with open(comment_file_path, "w") as comment_file: + for conflict in conflicts: + (filename_hash,) = run_command(f"echo -n {conflict} | sha256sum | cut -d' ' -f1") + comment_file.write( + f"[Src]({compare}/{merge_base}..{src_branch}#diff-{filename_hash}) " + f"[Dst]({compare}/{merge_base}..{dst_branch}#diff-{filename_hash}) " + f"{conflict}\n" + ) + run_command(f"gh pr comment -F {comment_file_path}") + os.remove(comment_file_path) + + os.remove(conflicts_file) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Merge a branch into another branch.") + parser.add_argument("--src", type=str, help="The source branch to merge.") + parser.add_argument( + "--dst", + type=str, + default=None, + help=( + "The destination branch to merge into. If no branch explicitly provided, uses the " + f"destination branch registered for the source branch in {MERGE_PATHS_FILE}." + ), + ) + args = parser.parse_args() + + merge_branches(src_branch=args.src, dst_branch=args.dst) diff --git a/scripts/merge_paths.json b/scripts/merge_paths.json new file mode 100644 index 00000000..34de8855 --- /dev/null +++ b/scripts/merge_paths.json @@ -0,0 +1,3 @@ +{ + "main-v0.13.1": "main" +} diff --git a/scripts/merge_paths_test.py b/scripts/merge_paths_test.py new file mode 100755 index 00000000..3b083904 --- /dev/null +++ b/scripts/merge_paths_test.py @@ -0,0 +1,25 @@ +from merge_branches import FINAL_BRANCH, MERGE_PATHS_FILE, load_merge_paths + + +def test_linear_path(): + merge_paths = load_merge_paths() + + src_dst_iter = iter(merge_paths.items()) + (oldest_branch, prev_dst_branch) = next(src_dst_iter) + assert ( + oldest_branch not in merge_paths.values() + ), f"Oldest branch '{oldest_branch}' cannot be a destination branch." + + for src_branch, dst_branch in src_dst_iter: + assert ( + prev_dst_branch == src_branch + ), ( + f"Since the merge graph is linear, the source branch '{src_branch}' must be the same " + f"as the previous destination branch, which is '{prev_dst_branch}'. Check out " + f"{MERGE_PATHS_FILE}." + ) + prev_dst_branch = dst_branch + + assert ( + prev_dst_branch == FINAL_BRANCH + ), f"The last destination is '{prev_dst_branch}' but must be '{FINAL_BRANCH}'." diff --git a/scripts/rust_fmt.sh b/scripts/rust_fmt.sh new file mode 100755 index 00000000..00c736e8 --- /dev/null +++ b/scripts/rust_fmt.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo +nightly-2024-01-12 fmt --all -- "$@"