From 899794ecada8890267e4b2bfb7696736e74815ad Mon Sep 17 00:00:00 2001 From: JP <36560907+0xfourzerofour@users.noreply.github.com> Date: Fri, 23 Feb 2024 11:07:29 -0500 Subject: [PATCH] feat(cross-arch): add cross arch release pipeline (#487) --- .github/workflows/docker-release.yaml | 62 +++++ .github/workflows/release.yaml | 226 ++++++++++++++++++ .gitignore | 3 + Cross.toml | 2 + Dockerfile | 6 +- Dockerfile.build | 18 ++ Dockerfile.cross | 15 ++ Makefile | 41 ++++ crates/types/build.rs | 9 - docs/README.md | 2 + docs/docker.md | 49 ++++ docs/release.md | 39 +++ .../rundler-launcher/docker-compose.yml | 2 +- 13 files changed, 461 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/docker-release.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 Cross.toml create mode 100644 Dockerfile.build create mode 100644 Dockerfile.cross create mode 100644 docs/release.md diff --git a/.github/workflows/docker-release.yaml b/.github/workflows/docker-release.yaml new file mode 100644 index 000000000..f14b14b3a --- /dev/null +++ b/.github/workflows/docker-release.yaml @@ -0,0 +1,62 @@ +name: Docker release + +on: + workflow_dispatch: + inputs: + version: + required: true + type: string + push: + tags: + - v*.*.* + +env: + CARGO_TERM_COLOR: always + DOCKER_IMAGE_NAME: alchemyplatform/rundler + +jobs: + build: + name: build and push + runs-on: ubuntu-22.04 + permissions: + packages: write + contents: read + steps: + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + toolchain: 1.71.0 + + - name: Install toolchain (nightly) + run: rustup toolchain add nightly --component rustfmt --profile minimal + + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: alchemyplatform + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker builder + run: | + docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64 + docker buildx create --use --name cross-builder + + - name: Build and push image + run: | + cargo install cross --git https://github.com/cross-rs/cross + + if [ -n "${{ github.event.inputs.version }}" ]; then + make GIT_TAG="${{ github.event.inputs.version }}" docker-build + else + make docker-build-latest + fi diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..c3d7d9c5f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,226 @@ +## This release action is inspired from https://githug.com/paradigmxyz/reth + +name: Release + +on: + workflow_dispatch: + inputs: + version: + required: true + type: string + push: + tags: + - v*.*.* + +jobs: + extract-version: + name: extract version + runs-on: ubuntu-latest + steps: + - name: Extract version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + else + echo "VERSION=$(echo ${GITHUB_REF#refs/tags/})" >> $GITHUB_OUTPUT + fi + id: extract_version + outputs: + VERSION: ${{ steps.extract_version.outputs.VERSION }} + + build: + name: build release + strategy: + matrix: + arch: + [ + aarch64-unknown-linux-gnu, + x86_64-unknown-linux-gnu, + x86_64-apple-darwin, + aarch64-apple-darwin, + x86_64-pc-windows-gnu, + ] + include: + - arch: aarch64-unknown-linux-gnu + platform: ubuntu-latest + profile: release + - arch: x86_64-unknown-linux-gnu + platform: ubuntu-latest + profile: release + - arch: x86_64-apple-darwin + platform: macos-latest + profile: release + - arch: aarch64-apple-darwin + platform: macos-latest + profile: release + - arch: x86_64-pc-windows-gnu + platform: ubuntu-latest + profile: release + + runs-on: ${{ matrix.platform }} + needs: extract-version + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - name: Get latest version of stable Rust + run: rustup update stable + - name: Install target + run: rustup target add ${{ matrix.arch }} + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + + # ============================== + # Apple Silicon SDK setup + # ============================== + + - name: Apple Silicon setup + if: ${{ matrix.job.target == 'aarch64-apple-darwin' }} + run: | + echo "SDKROOT=$(xcrun -sdk macosx --show-sdk-path)" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV + + # ============================== + # Builds + # ============================== + + - name: Build rundler for ${{ matrix.arch }} + run: | + cargo install cross + env PROFILE=${{ matrix.profile }} make build-${{ matrix.arch }} + + - name: Move cross-compiled binary + if: matrix.arch != 'x86_64-pc-windows-gnu' + run: | + mkdir artifacts + mv target/${{ matrix.arch }}/${{ matrix.profile }}/rundler ./artifacts + + - name: Move cross-compiled binary (Windows) + if: matrix.arch == 'x86_64-pc-windows-gnu' + run: | + mkdir artifacts + mv target/${{ matrix.arch }}/${{ matrix.profile }}/rundler.exe ./artifacts + + # ============================== + # Signing + # ============================== + + - name: Configure GPG and create artifacts + env: + GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + export GPG_TTY=$(tty) + echo -n "$GPG_SIGNING_KEY" | gpg --batch --import + cd artifacts + tar -czf rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz rundler* + echo "$GPG_PASSPHRASE" | gpg --passphrase-fd 0 --pinentry-mode loopback --batch -ab rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz + mv *tar.gz* .. + shell: bash + + # ======================================================================= + # Upload artifacts + # This is required to share artifacts between different jobs + # ======================================================================= + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz + path: rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz + + - name: Upload signature + uses: actions/upload-artifact@v3 + with: + name: rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz.asc + path: rundler-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.arch }}.tar.gz.asc + + draft-release: + name: draft release + needs: [build, extract-version] + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.extract-version.outputs.VERSION }} + permissions: + # Required to post the release + contents: write + steps: + # This is necessary for generating the changelog. It has to come before "Download Artifacts" or else it deletes the artifacts. + - name: Checkout sources + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # ============================== + # Download artifacts + # ============================== + - name: Download artifacts + uses: actions/download-artifact@v3 + + # ============================== + # Create release draft + # ============================== + - name: Generate full changelog + id: changelog + run: | + echo "CHANGELOG<> $GITHUB_OUTPUT + echo "$(git log --pretty=format:"- %s" $(git describe --tags --abbrev=0 ${{ env.VERSION }}^)..${{ env.VERSION }})" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create release draft + env: + GITHUB_USER: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # The formatting here is borrowed from Reth & Lighthouse (which is borrowed from OpenEthereum): + # https://github.com/openethereum/openethereum/blob/main/.github/workflows/build.yml + run: | + body=$(cat <<- "ENDBODY" + + + ## Testing Checklist (DELETE ME) + + - [ ] Run on testnet for 1-3 days. + - [ ] Ensure all CI checks pass. + + ## Release Checklist (DELETE ME) + + - [ ] Ensure all crates have had their versions bumped. + - [ ] Write the summary. + - [ ] Ensure all binaries have been added. + - [ ] Prepare release posts (Twitter, ...). + + ## Summary + + Add a summary, including: + + - Critical bug fixes + - New features + - Any breaking changes (and what to expect) + + ## All Changes + + ${{ steps.changelog.outputs.CHANGELOG }} + + ## Binaries + + The binaries are signed with the PGP key: `85C5 DEF0 37D3 FDE4 FC17 94B1 475B 35EA 9352 EB2` + + | System | Architecture | Binary | PGP Signature | + |:---:|:---:|:---:|:---| + | | x86_64 | [rundler-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-unknown-linux-gnu.tar.gz.asc) | + | | aarch64 | [rundler-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-aarch64-unknown-linux-gnu.tar.gz.asc) | + | | x86_64 | [rundler-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-pc-windows-gnu.tar.gz.asc) | + | | x86_64 | [rundler-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-x86_64-apple-darwin.tar.gz.asc) | + | | aarch64 | [rundler-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz) | [PGP Signature](https://github.com/${{ env.REPO_NAME }}/releases/download/${{ env.VERSION }}/rundler-${{ env.VERSION }}-aarch64-apple-darwin.tar.gz.asc) | + | | | | | + | **System** | **Option** | - | **Resource** | + | | Docker | [${{ env.VERSION }}](https://hub.docker.com/r/alchemyplatform/rundler) | + + ENDBODY + ) + assets=() + for asset in ./rundler-*.tar.gz*; do + assets+=("-a" "$asset/$asset") + done + tag_name="${{ env.VERSION }}" + echo "$body" | gh release create --draft "${assets[@]}" -F "-" "$tag_name" diff --git a/.gitignore b/.gitignore index d3b0897d5..728fbc633 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ *.profraw .DS_Store .helix + +# Release artifacts +dist/ diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 000000000..76c3e817f --- /dev/null +++ b/Cross.toml @@ -0,0 +1,2 @@ +[build] +dockerfile = "Dockerfile.build" diff --git a/Dockerfile b/Dockerfile index 177fb5e20..095574321 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Adapted from https://github.com/paradigmxyz/reth/blob/main/Dockerfile # syntax=docker/dockerfile:1.4 -FROM rust:1.75.0 AS chef-builder +FROM --platform=$TARGETPLATFORM rust:1.75.0 AS chef-builder # Install system dependencies RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list @@ -50,8 +50,8 @@ WORKDIR /app # install curl for healthcheck RUN apt-get -y update; apt-get -y install curl -# Copy reth over from the build stage +# Copy rundler over from the build stage COPY --from=builder /app/target/release/rundler /usr/local/bin EXPOSE 3000 8080 -CMD ["/usr/local/bin/rundler", "node"] +ENTRYPOINT ["/usr/local/bin/rundler"] diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 000000000..7b8f19e4e --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,18 @@ +ARG CROSS_BASE_IMAGE +# Dockerfile.forge +FROM ghcr.io/foundry-rs/foundry:latest as foundry + +FROM $CROSS_BASE_IMAGE +COPY --from=foundry /usr/local/bin/forge /usr/local/bin/forge + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y gnupg2 apt-transport-https ca-certificates software-properties-common +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config protobuf-compiler nodejs yarn +RUN add-apt-repository ppa:ethereum/ethereum +RUN apt-get update +RUN apt-get install -y solc diff --git a/Dockerfile.cross b/Dockerfile.cross new file mode 100644 index 000000000..1b2aa672e --- /dev/null +++ b/Dockerfile.cross @@ -0,0 +1,15 @@ +# This image is meant to enable cross-architecture builds. +# It assumes the rundler binary has already been compiled for `$TARGETPLATFORM` and is +# locatable in `./dist/bin/$TARGETARCH` +FROM --platform=$TARGETPLATFORM ubuntu:22.04 + +LABEL org.opencontainers.image.source=https://github.com/alchemyplatform/rundler +LABEL org.opencontainers.image.licenses="GNU Lesser General Public License v3.0" + +# Filled by docker buildx +ARG TARGETARCH + +COPY ./dist/bin/$TARGETARCH/rundler /usr/local/bin/rundler + +EXPOSE 3000 8080 +ENTRYPOINT ["/usr/local/bin/rundler"] diff --git a/Makefile b/Makefile index 75357566d..c176f6a86 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,11 @@ ##@ Test UNIT_TEST_ARGS := --locked --workspace --all-features +PROFILE ?= release +DOCKER_IMAGE_NAME ?= alchemyplatform/rundler +BIN_DIR = "dist/bin" +BUILD_PATH = "target" +GIT_TAG ?= $(shell git describe --tags --abbrev=0) .PHONY: build build: ## Build the project. @@ -28,8 +33,44 @@ test-spec-integrated: ## Run spec tests in integrated mode .PHONY: test-spec-modular test-spec-modular: ## Run spec tests in modular mode test/spec-tests/remote/run-spec-tests.sh + +.PHONY: submodule-update +submodule-update: ## Update git submodules + git submodule update + +build-%: + cross build --bin rundler --target $* --profile "$(PROFILE)" .PHONY: fmt fmt: ## format code with nightly rust cargo +nightly fmt +# Note: This requires a buildx builder with emulation support. For example: +# +# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64` +# `docker buildx create --use --driver docker-container --name cross-builder` +.PHONY: docker-build-latest +docker-build-latest: ## Build and push a cross-arch Docker image tagged with the latest git tag and `latest`. + $(call build_docker_image,$(GIT_TAG),latest) + +.PHONY: docker-build +docker-build: ## Build and push a cross-arch Docker image + $(call build_docker_image,$(GIT_TAG)) + +# Create a cross-arch Docker image with the given tags and push it +define build_docker_image + $(MAKE) build-aarch64-unknown-linux-gnu + mkdir -p $(BIN_DIR)/arm64 + cp $(BUILD_PATH)/aarch64-unknown-linux-gnu/$(PROFILE)/rundler $(BIN_DIR)/arm64/rundler + + $(MAKE) build-x86_64-unknown-linux-gnu + mkdir -p $(BIN_DIR)/amd64 + cp $(BUILD_PATH)/x86_64-unknown-linux-gnu/$(PROFILE)/rundler $(BIN_DIR)/amd64/rundler + + docker buildx build --file ./Dockerfile.cross . \ + --platform linux/arm64,linux/amd64 \ + --tag $(DOCKER_IMAGE_NAME):$(1) \ + $(if $(2),--tag $(DOCKER_IMAGE_NAME):$(2)) \ + --provenance=false +endef + diff --git a/crates/types/build.rs b/crates/types/build.rs index 47c80412c..9a43ee637 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -19,7 +19,6 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=contracts/lib"); println!("cargo:rerun-if-changed=contracts/src"); println!("cargo:rerun-if-changed=contracts/foundry.toml"); - update_submodules()?; generate_contract_bindings()?; Ok(()) } @@ -64,14 +63,6 @@ fn generate_abis() -> Result<(), Box> { ) } -fn update_submodules() -> Result<(), Box> { - run_command( - Command::new("git").arg("submodule").arg("update"), - "https://github.com/git-guides/install-git", - "update submodules", - ) -} - fn run_command( command: &mut Command, install_page_url: &str, diff --git a/docs/README.md b/docs/README.md index b51b61da3..c20dea761 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,3 +11,5 @@ [Docker](./docker.md): Instructions for building and running Docker images. [Proto](./proto.md): Protobuf usage and best practices. + +[Releases](./release.md): The release pipeline for the rundler binary diff --git a/docs/docker.md b/docs/docker.md index 243df9224..2052384b6 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -31,3 +31,52 @@ services: ``` An example docker-compose configuration running Rundler in its distributed mode can be found [here](../test/spec-tests/remote/docker-compose.yml). + +## Cross-Platform Docker Builds with Docker and cross-rs + +### Prerequisites + +- [cross-rs](https://github.com/cross-rs/cross) +- [tonistiigi/binfmt](https://github.com/tonistiigi/binfmt) +- [docker-buildx](https://github.com/docker/buildx) + +### Build Phase [Dockerfile.build](../Dockerfile.build) + +This phase compiles and imports required libraries for successful compilation. It uses the Dockerfile.build as an environment. The base image is specified by the `CROSS_BASE_IMAGE` argument. A list of images that `cross-rs` provides can be found [here](https://github.com/cross-rs/cross/tree/main/docker). + +### Release Phase [Dockerfile.cross](../Dockerfile.cross) + +This phase imports the compiled binary from the previous stage into its environment and exposes relevant ports for the correct functioning of the program. The target platform is specified by the `TARGETPLATFORM` argument. + +### Usage + +**GitHub Actions** + +``` +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Set up Docker builder + run: | + docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64 + docker buildx create --use --name cross-builder + + - name: Build and push image + run: | + cargo install cross --git https://github.com/cross-rs/cross + sudo -E env "PATH=$PATH" make docker-build-latest +``` + +**Local Builds** + +These command should only be used if you are trying to cross compile the application locally. If you just want to build cross compiled docker images, you should use the commands above. + + +``` +docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64 +docker buildx create --use --name cross-builder +cargo install cross --git https://github.com/cross-rs/cross +sudo -E env "PATH=$PATH" make docker-build-latest +``` diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 000000000..2f47944a0 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,39 @@ +# Releases + +The rundler project's releases are seamlessly managed by GitHub Actions and can be accessed here. This comprehensive workflow orchestrates multiple steps to compile and release new versions of the rundler project. + +## Workflow Steps + +# Extract Version + +This initial step conditionally extracts the project version either from GitHub Actions inputs or the Git reference. + +# Build + +The build process is orchestrated to cater to various architectures and platforms. Using a dynamic matrix strategy, it defines distinct build configurations encompassing architecture, platform, and profile. Key actions include: + +- Checking out the source code. +- Updating the Rust toolchain. +- Installing the target architecture. +- Leveraging the `Swatinem/rust-cache` action to efficiently cache Rust dependencies. +- Setting up essential environment variables for Apple Silicon SDK during Apple builds. +- Compiling the project with the specified profile and architecture. +- Organizing the compiled binary into a designated 'artifacts' directory. +- Adapting Windows builds by appending a '.exe' extension to the binary. + +# Signing + +Ensuring the integrity of the release, this step imports the GPG signing key and passphrase securely from GitHub secrets. It then generates GPG signatures for the resulting tarballs, strategically placing them in the root directory. + +# Upload Artifacts + +Leveraging the `actions/upload-artifact` action, this step uploads compressed artifacts along with their corresponding signatures to GitHub Actions. + +# Draft Release + +Dependent on the successful completion of the 'build' and 'extract-version' steps, this final step seamlessly manages the release draft. Actions include: + +- Checking out the source code for the release process. +- Downloading the artifacts necessary for the release. +- Constructing a detailed changelog by extracting commit messages between the current and previous versions. +- Initiating the creation of a draft release on GitHub. The release template includes the changelog and convenient download links for the signed tarballs. diff --git a/test/spec-tests/launchers/rundler-launcher/docker-compose.yml b/test/spec-tests/launchers/rundler-launcher/docker-compose.yml index f3dc3349b..6f50c1fe9 100644 --- a/test/spec-tests/launchers/rundler-launcher/docker-compose.yml +++ b/test/spec-tests/launchers/rundler-launcher/docker-compose.yml @@ -6,7 +6,7 @@ services: ports: - "3000:3000" - "8080:8080" - command: bash -c "/usr/local/bin/rundler node" + command: node environment: - RUST_LOG=debug - ENTRY_POINTS=0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789