From 2fd05c00da6bb2e805613bea9ab3c266f677f7e6 Mon Sep 17 00:00:00 2001 From: hawthorne-abendsen Date: Fri, 5 Apr 2024 22:23:34 +0300 Subject: [PATCH] init commit --- .github/workflows/docker-release.yml | 47 ++++++++ .github/workflows/release.yml | 89 +++++++++++++++ README.md | 87 ++++++++++++++ docker/Dockerfile | 23 ++++ docker/entrypoint.sh | 162 +++++++++++++++++++++++++++ 5 files changed, 408 insertions(+) create mode 100644 .github/workflows/docker-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 README.md create mode 100644 docker/Dockerfile create mode 100644 docker/entrypoint.sh diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..8abd35d --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,47 @@ +name: Build, Package and Release + +on: + push: + tags: + - 'v*' + +jobs: + build_and_docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Extract version tag + run: echo "VERSION_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Log in to GHCR using a PAT + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + context: ./docker + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ github.sha }} + ghcr.io/${{ github.repository }}:${{ env.VERSION_TAG }} + ghcr.io/${{ github.repository }}:latest + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.VERSION_TAG }} + draft: false + prerelease: false \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..30283ca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,89 @@ +name: Build and Release Contract + +on: + workflow_call: + inputs: + build_path: + description: 'JSON-encoded array of relative path to the contract directories' + type: string + required: true + default: '[""]' + release_name: + description: 'Name for the release' + required: true + type: string + release_description: + description: 'Description for the release' + required: false + type: string + secrets: + release_token: + description: 'Github token' + required: true + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + directory: ${{ fromJson(inputs.build_path) }} + steps: + - name: Set build directory name + run: | + build_dir_name="build_${{ strategy.job-index }}" + echo "BUILD_DIR_NAME=$build_dir_name" >> $GITHUB_ENV + echo "BUILD_DIR_PATH=${{ github.workspace }}/$build_dir_name" >> $GITHUB_ENV + + - name: Verify that checkout directory doesn't exist + run: | + if [[ -d ${{ env.BUILD_DIR_PATH }} ]]; then + echo "Directory ${{ env.BUILD_DIR_PATH }} already exists" + exit 1 + fi + + - name: Checkout code + uses: actions/checkout@v4 + with: + path: ${{ env.BUILD_DIR_NAME }} + + - name: Run docker container + working-directory: ${{ env.BUILD_DIR_PATH }} + run: docker run --rm -e CONTRACT_DIR=${{ matrix.directory }} -v "${{ env.BUILD_DIR_PATH }}:/inspector/home" ghcr.io/stellar-expert/soroban-build-workflow:latest + + - name: Get wasm file name + working-directory: ${{ env.BUILD_DIR_PATH }} + run: | + cd ${{ env.BUILD_DIR_PATH }}/release + wasm_file=$(find -type f -name "*.wasm") + cp $wasm_file ${{ env.BUILD_DIR_PATH }} + echo "WASM_FILE_NAME=$(basename $wasm_file)" >> $GITHUB_ENV + echo "WASM_FILE_SHA256=$(sha256sum $wasm_file | cut -d ' ' -f 1)" >> $GITHUB_ENV + + - name: Wasm file from directory "${{ matrix.directory }}" with hash ${{ env.WASM_FILE_SHA256 }} created + run: echo ${{ env.WASM_FILE_NAME }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.WASM_FILE_NAME }} + path: ${{ env.BUILD_DIR_PATH }}/release/${{ env.WASM_FILE_NAME }} + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ inputs.release_name }} + draft: false + prerelease: false + body: ${{ inputs.release_description }} + files: '**/*.wasm' + token: ${{ secrets.release_token }} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..beae333 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Soroban Smart Contract Compilation Workflow + +Reusable GitHub Actions workflow that streamlines the compilation and release process of Stellar smart contracts for Soroban WASM runtime. + +When triggered, this workflow: +- Compiles a smart contract (or multiple contracts) in the repository +- Creates an optimized WebAssembly file ready to be deployed to Soroban +- Publishes GitHub release with attached build artifacts +- Includes SHA256 hashes of complied WASM files into actions output for further verification + +## Configuration + +### Prerequisites + +- Create a GitHub Actions workflow file `.github/workflows/release.yml` in your repository. +- Decide how the compliation workflow will be triggered. The recommended way is to configure workflow activation on git tag creation. This should simplify versioning and ensure unique release names. + +### Workflow inputs and secrets + +Basic compilation workflow path: +`stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main` + +The workflow expects the following inputs in the `with` section: +- `release_name` (required) - release name template that includes a release version variable, e.g. `${{ github.ref_name }}` +- `build_path` - array of contract relative paths to compile, defaults to the repository root directory +- `release_description` - optional text to attach to a relase description + +### Basic workflow for the reporisotry with a single contract + +```yaml +name: Build and Release # name it whatever you like +on: + push: + tags: + - 'v*' # triggered whenever a new tag (previxed with "v") is pushed to the repository +jobs: + release_contracts: + uses: stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main + with: + release_name: ${{ github.ref_name }} # use git tag as unique release name + release_description: 'Contract release' # some boring placeholder text to attach + build_path: '["src/my-awesome-contract"]' # relative path to your really awesome contract + secrets: # the authentication token will be automatically created by GitHub + release_token: ${{ secrets.GITHUB_TOKEN }} # don't modify this line +``` + +### Building multiple contracts + +To build multiple contracts at once, include all relative paths of the subdirectories containing contract sources +to the `build_path` array. For example, + +```yaml +jobs: + release_contracts: + with: # build contracts located in "/src/token", "/src/dao/contract", and the repository root directory + build_path: '["src/token", "src/dao/dao", ""]' +``` + +### Triggering build process manually + +Triggering this workflow manually requires a unique release name prompt. Replace the trigger condition in config +and update `release_name` to utilize the value from the prompt. + +```yaml +on: + workflow_dispatch: + inputs: + release_name: + description: 'Unique release name' + required: true + type: string +jobs: + release-contract: + with: + release_name: ${{ github.event.inputs.release_name }} +``` + +## Notes + +- The workflow assumes that each contract directory contains the necessary structure and files for your build process. +- If you want to run your contract tests automatically before deploying the contract, add corresponding action invocation + before the `release_contracts` job to make sure that broken contracts won't end up in published releases. +- In case of the multi-contract repository setup, contracts shouldn't have the same name (defined in TOML file) and version + to avoid conflicts during the release process. +- To enable automatic contract source validation process, contracts should be deployed to Stellar Network directly + from a complied GitHub realese generated by this workflow. Otherwise the deployed contract hash may not + match the release artifcats due to the compilation environment variations. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..8f26211 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,23 @@ +# Start from an Ubuntu base image +FROM ubuntu:20.04 + +# Disable prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Set up working directory +WORKDIR /inspector + +# Install packages +RUN apt-get update && \ + apt-get install -y git curl wget jq build-essential && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Specify the contract directory +ENV CONTRACT_DIR=${CONTRACT_DIR} + +# Copy entrypoint script +COPY /entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..5e430b7 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,162 @@ +#!/bin/bash + +# Set mount directory +MOUNT_DIR="/inspector/home" + +# Check if the MOUNT_DIR directory is a mount point +if ! mountpoint -q "$MOUNT_DIR"; then + echo "ERROR: $MOUNT_DIR is not mounted!" + exit 1 +fi + +cd $MOUNT_DIR + +# Navigate to the contract directory if defined +if [ "${CONTRACT_DIR}" ]; then + # Verify that the contract directory exists + if [ ! -d "${CONTRACT_DIR}" ]; then + echo "ERROR: Contract directory ${CONTRACT_DIR} does not exist" + exit 1 + fi + + # Navigate to the contract directory only if it's not the root + cd ${CONTRACT_DIR} + echo "Current directory: $(pwd)" + # Current directory files + ls -la +fi + +# Check if the Cargo.toml file exists +if [ ! -f "Cargo.toml" ]; then + echo "ERROR: Cargo.toml file does not exist" + exit 1 +fi + +RUST_VERSION=$(sed -n '/\[package\]/,/^$/{/rust-version = /{s/rust-version = "\(.*\)"/\1/p;}}' Cargo.toml) + +# If rust version is not found in Cargo.toml, set it to the default version +if [ -z "$RUST_VERSION" ]; then + RUST_VERSION="beta" +fi +echo "Required Rust version: $RUST_VERSION" + +# Installing rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . $HOME/.cargo/env + +# Verify that rustup was installed +if [ $? -ne 0 ]; then + echo "ERROR: Failed to install rustup" + exit 1 +fi + +# Install the required rust version +rustup install $RUST_VERSION + +# Verify that rust was installed +if [ $? -ne 0 ]; then + echo "ERROR: Failed to install rust" + exit 1 +fi + +# Set the default rust version +rustup default $RUST_VERSION + +# Verify that rust was installed +if [ $? -ne 0 ]; then + echo "ERROR: Failed to set the default rust version" + exit 1 +fi + +# Add the cargo bin directory to the PATH +PATH="/root/.cargo/bin:${PATH}" + +# Add the wasm32-unknown-unknown target +rustup target add wasm32-unknown-unknown + +# Verify that wasm32-unknown-unknown target was added +if [ $? -ne 0 ]; then + echo "ERROR: Failed to add wasm32-unknown-unknown target" + exit 1 +fi + +# Install wasm-opt +wget https://github.com/WebAssembly/binaryen/releases/download/version_101/binaryen-version_101-x86_64-linux.tar.gz && \ + tar -xzf binaryen-version_101-x86_64-linux.tar.gz && \ + cp binaryen-version_101/bin/wasm-opt /usr/local/bin/ && \ + rm -rf binaryen-version_101 binaryen-version_101-x86_64-linux.tar.gz + +# Verify that wasm-opt was installed +if [ $? -ne 0 ]; then + echo "ERROR: Failed to install wasm-opt" + exit 1 +fi + +echo "Rustc Version:" $(rustc --version) && echo "Cargo Version:" $(cargo --version) + +# Build the project with cargo for wasm32-unknown-unknown target +cargo build --target wasm32-unknown-unknown --release + +# Verify that the build was successful +if [ $? -ne 0 ]; then + echo "ERROR: Failed to build the project" + exit 1 +fi + +echo "Rustc Version:" $(rustc --version) && echo "Cargo Version:" $(cargo --version) + +# Check if the Cargo.toml file exists +if [ ! -f "Cargo.toml" ]; then + echo "ERROR: Cargo.toml file does not exist" + exit 1 +fi + +# Get the target directory +TARGET_DIR=$(cargo metadata --format-version=1 --no-deps | jq -r ".target_directory") + +# Verify that the target directory exists +if [ ! -d "${TARGET_DIR}" ]; then + echo "ERROR: Target directory ${TARGET_DIR} does not exist" + exit 1 +fi + +# Create the release directory +mkdir -p ${MOUNT_DIR}/release + +# Verify that the release directory was created +if [ ! -d "${MOUNT_DIR}/release" ]; then + echo "ERROR: Failed to create the release directory" + exit 1 +fi + +# Get the package name and version +PACKAGE_NAME=$(grep -m1 '^name =' Cargo.toml | sed -E 's/^name = "(.*)"$/\1/') +PACKAGE_VERSION=$(grep -m1 '^version =' Cargo.toml | sed -E 's/^version = "(.*)"$/\1/') + +# Verify that the package name and version were found +if [ -z "$PACKAGE_NAME" ] || [ -z "$PACKAGE_VERSION" ]; then + echo "ERROR: Failed to get the package name and version" + exit 1 +fi + +WASM_FILE_NAME="${PACKAGE_NAME}_v${PACKAGE_VERSION}.wasm" + +# Find the .wasm file and copy it as unoptimized.wasm for hash calculation +find ${TARGET_DIR}/wasm32-unknown-unknown/release -name "*.wasm" -exec cp {} ${MOUNT_DIR}/release/${WASM_FILE_NAME} \; + +# Verify that the unoptimized.wasm file exists +if [ ! -f "$MOUNT_DIR/release/${WASM_FILE_NAME}" ]; then + echo "ERROR: unoptimized.wasm file does not exist" + exit 1 +fi + +# Navigate to the release directory +cd ${MOUNT_DIR}/release + +# Optimize the WASM file +wasm-opt -Oz ${WASM_FILE_NAME} -o ${WASM_FILE_NAME} + +# Verify that the optimized.wasm file exists +if [ ! -f "${MOUNT_DIR}/release/${WASM_FILE_NAME}" ]; then + echo "ERROR: optimized.wasm file does not exist" + exit 1 +fi \ No newline at end of file