diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 6feba12..201a583 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -7,6 +7,10 @@ on: description: 'Name for the release' required: true type: string + soroban_cli_version: + description: 'Soroban CLI version' + required: false + type: string permissions: contents: write @@ -57,6 +61,8 @@ jobs: with: context: ./docker push: true + build-args: | + SOROBAN_CLI_VERSION=${{ inputs.soroban_cli_version }} tags: | ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:${{ inputs.release_name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 92398a5..d26bce5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,11 +3,18 @@ name: Build and Release Contract on: workflow_call: inputs: - build_path: - description: 'JSON-encoded array of relative path to the contract directories' + relative_path: + description: 'Relative path to the working directory' type: string - required: true - default: '[""]' + required: false + make_target: + description: 'Make target for the contract' + type: string + required: false + package: + description: 'Package to build' + type: string + required: false release_name: description: 'Name for the release' required: true @@ -24,16 +31,12 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - directory: ${{ fromJson(inputs.build_path) }} steps: - name: Set directory names and paths run: | - build_dir_name="build_${{ strategy.job-index }}" + build_dir_name="build_"$(openssl rand -base64 8 | tr -d '/+=' | head -c 8) echo "BUILD_DIR_NAME=$build_dir_name" >> $GITHUB_ENV echo "BUILD_DIR_PATH=${{ github.workspace }}/$build_dir_name" >> $GITHUB_ENV - echo "CONTRACT_DIR_NAME=${{ matrix.directory || '/' }}" >> $GITHUB_ENV - name: Verify that checkout directory doesn't exist run: | @@ -49,42 +52,86 @@ jobs: - 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:v20.3.1 + run: docker run --rm -e RELATIVE_PATH=${{ inputs.relative_path }} -e MAKE_TARGET=${{ inputs.make_target }} -e PACKAGE=${{ inputs.package }} -v "${{ env.BUILD_DIR_PATH }}:/inspector/home" ghcr.io/stellar-expert/soroban-build-workflow:v20.3.1 - name: Get wasm file name working-directory: ${{ env.BUILD_DIR_PATH }} run: | - cd ${{ env.BUILD_DIR_PATH }}/release + cd ${{ env.BUILD_DIR_PATH }}/compilation_workflow_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: Compiled WASM ${{ env.WASM_FILE_SHA256 }} from ${{ env.CONTRACT_DIR_NAME }} - 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: Build release name + run: | + if [ -n "${{ inputs.relative_path }}" ]; then + relative_path=$(echo "_${{ inputs.relative_path }}" | sed 's/\W\+/_/g') + fi + tag_name="${{ inputs.release_name }}${relative_path}_${{ env.WASM_FILE_NAME }}" + echo "TAG_NAME=$tag_name" >> $GITHUB_ENV - name: Create release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ inputs.release_name }} + tag_name: ${{ env.TAG_NAME }} draft: false prerelease: false body: ${{ inputs.release_description }} - files: '**/*.wasm' - token: ${{ secrets.release_token }} \ No newline at end of file + files: '${{ env.BUILD_DIR_PATH }}/compilation_workflow_release/${{ env.WASM_FILE_NAME }}' + token: ${{ secrets.release_token }} + + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '14' + + - name: Build output + run: | + JSON_OUTPUT=$(node -e "console.log(JSON.stringify({ + wasm: process.env.WASM, + hash: process.env.HASH, + relPath: (process.env.REL_PATH || undefined), + package: (process.env.PACKAGE || undefined), + make: (process.env.MAKE || undefined) + }))") + echo "WASM_OUTPUT='$JSON_OUTPUT'" >> $GITHUB_ENV + env: + REL_PATH: ${{ inputs.relative_path }} + PACKAGE: ${{ inputs.package }} + MAKE: ${{ inputs.make_target }} + HASH: ${{ env.WASM_FILE_SHA256 }} + WASM: ${{ env.WASM_FILE_NAME }} + + - name: Output WASM ${{ env.WASM_OUTPUT }} + run: echo ${{ env.WASM_OUTPUT }} + + - name: Send release info + run: | + JSON_OBJECT=$(node -e "console.log(JSON.stringify({ + repository: process.env.REPOSITORY, + commitHash: process.env.COMMIT_HASH, + jobId: process.env.JOB_ID, + runId: process.env.RUN_ID, + contractHash: process.env.CONTRACT_HASH, + relativePath: process.env.RELATIVE_PATH || undefined, + packageName: process.env.PACKAGE_NAME || undefined, + makeTarget: process.env.MAKE_TARGET || undefined + }))") + + echo "JSON to send: $JSON_OBJECT" + + curl -X POST "https://api.stellar.expert/explorer/public/contract-validation/match" \ + -H "Content-Type: application/json" \ + -d "$JSON_OBJECT" \ + --max-time 15 + env: + REPOSITORY: ${{ github.server_url }}/${{ github.repository }} + COMMIT_HASH: ${{ github.sha }} + JOB_ID: ${{ github.job }} + RUN_ID: ${{ github.run_id }} + CONTRACT_HASH: ${{ env.WASM_FILE_SHA256 }} + RELATIVE_PATH: ${{ inputs.relative_path }} + PACKAGE_NAME: ${{ inputs.package }} + MAKE_TARGET: ${{ inputs.make_target }} diff --git a/README.md b/README.md index beae333..924c201 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ Basic compilation workflow path: 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 +- `package` (optional) - package name to build. Builds contract in working directory by default +- `relative_path` (optional) - relative path to the contract source directory. Defaults to the repository root directory +- `make_target` (optional) - make target to invoke. Not invoked by default. Useful for contracts with dependencies, that must be built before the main contract ### Basic workflow for the reporisotry with a single contract @@ -34,26 +35,57 @@ on: tags: - 'v*' # triggered whenever a new tag (previxed with "v") is pushed to the repository jobs: - release_contracts: + release-contract-a: 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 + release_name: ${{ github.ref_name }} # use git tag as unique release name + release_description: 'Contract release' # some boring placeholder text to attach + relative_path: '["src/my-awesome-contract"]' # relative path to your really awesome contract + package: 'my-awesome-contract' # package name to build + make_target: 'build-dependencies' # make target to invoke secrets: # the authentication token will be automatically created by GitHub - release_token: ${{ secrets.GITHUB_TOKEN }} # don't modify this line + 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, +To build multiple contracts, add a separate job for each contract. The workflow will compile and release each contract independently. ```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: - with: # build contracts located in "/src/token", "/src/dao/contract", and the repository root directory - build_path: '["src/token", "src/dao/dao", ""]' + release-contract-a: + uses: stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main + with: + release_name: ${{ github.ref_name }} + release_description: 'Awesome contract release' + relative_path: '["src/my-awesome-contract"]' + package: 'my-awesome-contract' + make_target: 'build-dependencies' + secrets: + release_token: ${{ secrets.GITHUB_TOKEN }} + + release-contract-b: + uses: stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main + with: + release_name: ${{ github.ref_name }} + release_description: 'Contract release' + relative_path: '["src/another-awesome-contract"]' + secrets: + release_token: ${{ secrets.GITHUB_TOKEN }} + + + release-contract-from-root: + uses: stellar-expert/soroban-build-workflow/.github/workflows/release.yml@main + with: + release_name: ${{ github.ref_name }} + release_description: 'Root contract release' + secrets: + release_token: ${{ secrets.GITHUB_TOKEN }} ``` ### Triggering build process manually @@ -79,7 +111,7 @@ jobs: - 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. + before the `release_contract` 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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 2c2f45b..c25ac46 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -9,7 +9,7 @@ WORKDIR /inspector # Install packages RUN apt-get update && \ - apt-get install -y git curl wget jq build-essential && \ + apt-get install -y git curl wget jq build-essential uuid-runtime && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -22,8 +22,15 @@ ENV PATH="${PATH}:/root/.cargo/bin" # Install the wasm32-unknown-unknown target RUN rustup target add wasm32-unknown-unknown +# Define the version argument for soroban-cli with a default value +ARG SOROBAN_CLI_VERSION="" + # Install soroban-cli -RUN cargo install --locked soroban-cli --features opt +RUN if [ -z "$SOROBAN_CLI_VERSION" ]; then \ + cargo install --locked soroban-cli --features opt; \ + else \ + cargo install --locked soroban-cli --features opt --version $SOROBAN_CLI_VERSION; \ + fi # Print the version of rustc RUN rustc --version @@ -34,11 +41,19 @@ RUN cargo --version # Print the version of soroban-cli RUN soroban --version -# Specify the contract directory -ENV CONTRACT_DIR=${CONTRACT_DIR} +# Specify the package to build +ENV PACKAGE=${PACKAGE} + +# Specify the make target +ENV MAKE_TARGET=${MAKE_TARGET} + +# Specify the working directory +ENV RELATIVE_PATH=${RELATIVE_PATH} # Copy entrypoint script COPY /entrypoint.sh /usr/local/bin/ + +# Make the entrypoint script executable 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 index 3008721..c8224d8 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -11,19 +11,22 @@ 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" +# Check if the RELATIVE_PATH is defined +if [ "$RELATIVE_PATH" ]; then + # Change the working directory + cd $RELATIVE_PATH +fi + +# Check if the MAKE_TARGET is defined +if [ "$MAKE_TARGET" ]; then + # Run the make command + make $MAKE_TARGET + if [ $? -eq 0 ]; then + echo "Make completed successfully." + else + echo "Make failed." 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 # Print Rust version @@ -41,37 +44,36 @@ if [ ! -f "Cargo.toml" ]; then exit 1 fi -# Build the contract -soroban contract build +# Create output directory +OUTPUT="${MOUNT_DIR}/$(uuidgen)" +mkdir -p ${OUTPUT} -# Verify that the build was successful +# Get metadata +CARGO_METADATA=$(cargo metadata --format-version=1 --no-deps) if [ $? -ne 0 ]; then - echo "ERROR: Failed to build the project" + echo "ERROR: Failed to get the package metadata" 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 +# Check if the PACKAGE is defined add it to the build command +if [ "$PACKAGE" ]; then + soroban contract build --package $PACKAGE --out-dir ${OUTPUT} + # Set the package name to the provided package name + PACKAGE_NAME=$PACKAGE + PACKAGE_VERSION=$(echo "$CARGO_METADATA" | jq '.packages[] | select(.name == "'"${PACKAGE_NAME}"'") | .version' | sed -e 's/"//g') +else + soroban contract build --out-dir ${OUTPUT} + # Get the package name from the Cargo.toml file + PACKAGE_NAME=$(grep -m1 '^name =' Cargo.toml | sed -E 's/^name = "(.*)"$/\1/') + PACKAGE_VERSION=$(grep -m1 '^version =' Cargo.toml | sed -E 's/^version = "(.*)"$/\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" +# Verify that the build was successful +if [ $? -ne 0 ]; then + echo "ERROR: Failed to build the project" 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" @@ -80,23 +82,39 @@ fi WASM_FILE_NAME="${PACKAGE_NAME}_v${PACKAGE_VERSION}.wasm" +RELEASE_DIR="${MOUNT_DIR}/compilation_workflow_release" + +# Remove the release directory if it exists, to avoid conflicts +if [ -d "${RELEASE_DIR}" ]; then + rm -rf ${RELEASE_DIR} +fi + +# Create the release directory +mkdir -p ${RELEASE_DIR} + +# Verify that the release directory was created +if [ ! -d "${RELEASE_DIR}" ]; then + echo "ERROR: Failed to create the release directory" + exit 1 +fi + # 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} \; +find ${OUTPUT} -name "*.wasm" -exec cp {} ${RELEASE_DIR}/${WASM_FILE_NAME} \; # Verify that the unoptimized.wasm file exists -if [ ! -f "$MOUNT_DIR/release/${WASM_FILE_NAME}" ]; then +if [ ! -f "${RELEASE_DIR}/${WASM_FILE_NAME}" ]; then echo "ERROR: unoptimized.wasm file does not exist" exit 1 fi # Navigate to the release directory -cd ${MOUNT_DIR}/release +cd ${RELEASE_DIR} # Optimize the WASM file soroban contract optimize --wasm ${WASM_FILE_NAME} --wasm-out ${WASM_FILE_NAME} # Verify that the optimized.wasm file exists -if [ ! -f "${MOUNT_DIR}/release/${WASM_FILE_NAME}" ]; then +if [ ! -f "${RELEASE_DIR}/${WASM_FILE_NAME}" ]; then echo "ERROR: optimized.wasm file does not exist" exit 1 fi \ No newline at end of file