diff --git a/.github/actions/validate-artifact-scope/action.yaml b/.github/actions/validate-artifact-scope/action.yaml new file mode 100644 index 00000000000..7440efc63a3 --- /dev/null +++ b/.github/actions/validate-artifact-scope/action.yaml @@ -0,0 +1,103 @@ +name: Validate Artifact Scope +description: Checks there are any modified Solidity files outside of the specified scope. If so, it prints a warning message, but does not fail the workflow. +inputs: + product: + description: The product for which the artifacts are being generated + required: true + sol_files: + description: Comma-separated (CSV) or space-separated (shell) list of Solidity files to check + required: true + +runs: + using: composite + steps: + - name: Transform input array + id: transform_input_array + shell: bash + run: | + is_csv_format() { + local input="$1" + if [[ "$input" =~ "," ]]; then + return 0 + else + return 1 + fi + } + + is_space_separated_string() { + local input="$1" + if [[ "$input" =~ ^[^[:space:]]+([[:space:]][^[:space:]]+)*$ ]]; then + return 0 + else + return 1 + fi + } + + array="${{ inputs.sol_files }}" + + if is_csv_format "$array"; then + echo "::debug::CSV format detected, nothing to do" + echo "sol_files=$array" >> $GITHUB_OUTPUT + exit 0 + fi + + if is_space_separated_string "$array"; then + echo "::debug::Space-separated format detected, converting to CSV" + csv_array="${array// /,}" + echo "sol_files=$csv_array" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "::error::Invalid input format for sol_files. Please provide a comma-separated (CSV) or space-separated (shell) list of Solidity files" + exit 1 + + - name: Check for changes outside of artifact scope + shell: bash + run: | + echo "::debug::All modified contracts:" + echo "${{ steps.transform_input_array.outputs.sol_files }}" | tr ',' '\n' + if [ "${{ inputs.product }}" = "shared" ]; then + excluded_paths_pattern="!/^contracts\/src\/v0\.8\/interfaces/ && !/^contracts\/src\/v0\.8\/${{ inputs.product }}/ && !/^contracts\/src\/v0\.8\/[^\/]+\.sol$/" + else + excluded_paths_pattern="!/^contracts\/src\/v0\.8\/${{ inputs.product }}/" + fi + echo "::debug::Excluded paths: $excluded_paths_pattern" + unexpected_files=$(echo "${{ steps.transform_input_array.outputs.sol_files }}" | tr ',' '\n' | awk "$excluded_paths_pattern") + missing_files="" + set -e + set -o pipefail + if [[ -n "$unexpected_files" ]]; then + products=() + productsStr="" + IFS=$'\n' read -r -d '' -a files <<< "$unexpected_files" || true + echo "Files: ${files[@]}" + + for file in "${files[@]}"; do + missing_files+="$file," + + product=$(echo "$file" | awk -F'src/v0.8/' '{if ($2 ~ /\//) print substr($2, 1, index($2, "/")-1); else print "shared"}') + if [[ ! " ${products[@]} " =~ " ${product} " ]]; then + products+=("$product") + productsStr+="$product, " + fi + done + productsStr=${productsStr%, } + + set +e + set +o pipefail + + missing_files=$(echo $missing_files | tr ',' '\n') + + echo "Error: Found modified contracts outside of the expected scope: ${{ inputs.product }}" + echo "Files:" + echo "$missing_files" + echo "Action required: If you want to generate artifacts for other products ($productsStr) run this workflow again with updated configuration" + + echo "# Warning!" >> $GITHUB_STEP_SUMMARY + echo "## Reason: Found modified contracts outside of the expected scope: ${{ inputs.product }}" >> $GITHUB_STEP_SUMMARY + echo "### Files:" >> $GITHUB_STEP_SUMMARY + echo "$missing_files" >> $GITHUB_STEP_SUMMARY + echo "## Action required: If you want to generate artifacts for other products ($productsStr) run this workflow again with updated configuration" >> $GITHUB_STEP_SUMMARY + else + echo "No unexpected files found." + fi diff --git a/.github/workflows/solidity-foundry-artifacts.yml b/.github/workflows/solidity-foundry-artifacts.yml new file mode 100644 index 00000000000..e489033d675 --- /dev/null +++ b/.github/workflows/solidity-foundry-artifacts.yml @@ -0,0 +1,371 @@ +name: Solidity Foundry Artifact Generation +on: + workflow_dispatch: + inputs: + product: + type: choice + description: 'product for which to generate artifacts; should be the same as Foundry profile' + required: true + options: + - "automation" + - "ccip" + - "functions" + - "keystone" + - "l2ep" + - "liquiditymanager" + - "llo-feeds" + - "operatorforwarder" + - "shared" + - "transmission" + - "vrf" + base_ref: + description: 'commit or tag to be used as base reference, when looking for modified Solidity files' + required: true + +env: + FOUNDRY_PROFILE: ci + +jobs: + changes: + name: Detect changes + runs-on: ubuntu-latest + outputs: + changes: ${{ steps.changes.outputs.sol }} + product_changes: ${{ steps.changes-transform.outputs.product_changes }} + product_files: ${{ steps.changes-transform.outputs.product_files }} + changeset_changes: ${{ steps.changes-dorny.outputs.changeset }} + changeset_files: ${{ steps.changes-dorny.outputs.changeset_files }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + - name: Find modified contracts + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changes + with: + list-files: 'csv' + base: ${{ inputs.base_ref }} + predicate-quantifier: every + filters: | + ignored: &ignored + - '!contracts/src/v0.8/**/test/**' + - '!contracts/src/v0.8/**/tests/**' + - '!contracts/src/v0.8/**/mock/**' + - '!contracts/src/v0.8/**/mocks/**' + - '!contracts/src/v0.8/**/*.t.sol' + - '!contracts/src/v0.8/*.t.sol' + - '!contracts/src/v0.8/**/testhelpers/**' + - '!contracts/src/v0.8/testhelpers/**' + - '!contracts/src/v0.8/vendor/**' + other_shared: + - modified|added: 'contracts/src/v0.8/(interfaces/**/*.sol|*.sol)' + - *ignored + sol: + - modified|added: 'contracts/src/v0.8/**/*.sol' + - *ignored + product: &product + - modified|added: 'contracts/src/v0.8/${{ inputs.product }}/**/*.sol' + - *ignored + changeset: + - modified|added: 'contracts/.changeset/!(README)*.md' + + # Manual transformation needed, because shared contracts have a different folder structure + - name: Transform modified files + id: changes-transform + shell: bash + run: | + if [ "${{ inputs.product }}" = "shared" ]; then + echo "::debug:: Product is shared, transforming changes" + if [[ "${{ steps.changes.outputs.product }}" == "true" && "${{ steps.changes.outputs.other_shared }}" == "true" ]]; then + echo "::debug:: Changes were found in 'shared' folder and in 'interfaces' and root folders" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }},${{ steps.changes.outputs.other_shared_files }}" >> $GITHUB_OUTPUT + elif [[ "${{ steps.changes.outputs.product }}" == "false" && "${{ steps.changes.outputs.other_shared }}" == "true" ]]; then + echo "::debug:: Only contracts in' interfaces' and root folders were modified" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.other_shared_files }}" >> $GITHUB_OUTPUT + elif [[ "${{ steps.changes.outputs.product }}" == "true" && "${{ steps.changes.outputs.other_shared }}" == "false" ]]; then + echo "::debug:: Only contracts in 'shared' folder were modified" + echo "product_changes=true" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }}" >> $GITHUB_OUTPUT + else + echo "::debug:: No contracts were modified" + echo "product_changes=false" >> $GITHUB_OUTPUT + echo "product_files=" >> $GITHUB_OUTPUT + fi + else + echo "product_changes=${{ steps.changes.outputs.product }}" >> $GITHUB_OUTPUT + echo "product_files=${{ steps.changes.outputs.product_files }}" >> $GITHUB_OUTPUT + fi + + - name: Check for changes outside of artifact scope + uses: ./.github/actions/validate-artifact-scope + if: ${{ steps.changes.outputs.sol == 'true' }} + with: + sol_files: ${{ steps.changes.outputs.sol_files }} + product: ${{ inputs.product }} + + gather-basic-info: + name: Gather basic info + if: ${{ needs.changes.outputs.product_changes == 'true' }} + runs-on: ubuntu-22.04 + needs: [ changes ] + outputs: + foundry_version: ${{ steps.extract-foundry-version.outputs.foundry-version }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + fetch-depth: 0 + + - name: Extract Foundry version + uses: ./.github/actions/detect-solidity-foundry-version + with: + working-directory: contracts + + - name: Copy modified changesets + if: ${{ needs.changes.outputs.changeset_changes == 'true' }} + run: | + mkdir -p contracts/changesets + files="${{ needs.changes.outputs.changeset_files }}" + IFS=",' + for changeset in $files; do + echo "::debug:: Copying $changeset" + cp $changeset contracts/changesets + done + + - name: Generate basic info and modified contracts list + shell: bash + run: | + echo "Commit SHA used to generate artifacts: ${{ github.sha }}" > contracts/commit_sha_base_ref.txt + echo "Base reference SHA used to find modified contracts: ${{ inputs.base_ref }}" >> contracts/commit_sha_base_ref.txt + + IFS=',' read -r -a modified_files <<< "${{ needs.changes.outputs.product_files }}" + echo "# Modified contracts:" > contracts/modified_contracts.md + for file in "${modified_files[@]}"; do + echo " - [$file](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/$file)" >> contracts/modified_contracts.md + echo "$file" >> contracts/modified_contracts.txt + done + + - name: Upload basic info and modified contracts list + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 2 + continue-on-error: true + with: + name: tmp-basic-info + path: | + contracts/modified_contracts.md + contracts/modified_contracts.txt + contracts/commit_sha_base_ref.txt + contracts/changesets + retention-days: 7 + + # some of the artifacts can only be generated on product level, and we cannot scope them to single contracts + # some product-level modifications might also require shared contracts changes, so if these happened we need to generate artifacts for shared contracts as well + coverage-and-book: + if: ${{ needs.changes.outputs.product_changes == 'true' }} + name: Generate Docs and Code Coverage reports + runs-on: ubuntu-22.04 + needs: [changes, gather-basic-info] + steps: + - name: Prepare exclusion list + id: prepare-exclusion-list + run: | + cat < coverage_exclusions.json + ["automation", "functions", "vrf"] + EOF + coverage_exclusions=$(cat coverage_exclusions.json | jq -c .) + echo "coverage_exclusions=$coverage_exclusions" >> $GITHUB_OUTPUT + + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Create directories + shell: bash + run: | + mkdir -p contracts/code-coverage + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ needs.gather-basic-info.outputs.foundry_version }} + + # required for code coverage report generation + - name: Setup LCOV + uses: hrishikesh-kadam/setup-lcov@f5da1b26b0dcf5d893077a3c4f29cf78079c841d # v1.0.0 + + - name: Run Forge build for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + run: | + forge --version + forge build + working-directory: contracts + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Run coverage for product contracts + if: ${{ !contains(fromJson(steps.prepare-exclusion-list.outputs.coverage_exclusions), inputs.product) && needs.changes.outputs.product_changes == 'true' }} + working-directory: contracts + run: forge coverage --report lcov --report-file code-coverage/lcov.info + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Generate Code Coverage HTML report for product contracts + if: ${{ !contains(fromJson(steps.prepare-exclusion-list.outputs.coverage_exclusions), inputs.product) && needs.changes.outputs.product_changes == 'true' }} + shell: bash + working-directory: contracts + run: genhtml code-coverage/lcov.info --branch-coverage --output-directory code-coverage + + - name: Run Forge doc for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + run: forge doc --build -o docs + working-directory: contracts + env: + FOUNDRY_PROFILE: ${{ inputs.product }} + + - name: Upload Artifacts for product contracts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 2 + continue-on-error: true + with: + name: tmp-${{ inputs.product }}-artifacts + path: | + contracts/docs + contracts/code-coverage/lcov-.info + contracts/code-coverage + retention-days: 7 + + # Generates UML diagrams and Slither reports for modified contracts + uml-static-analysis: + if: ${{ needs.changes.outputs.product_changes == 'true' }} + name: Generate UML and Slither reports for modified contracts + runs-on: ubuntu-22.04 + needs: [changes, gather-basic-info] + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout the repo + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + with: + fetch-depth: 0 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 # v1.2.0 + with: + version: ${{ needs.gather-basic-info.outputs.foundry_version }} + + - name: Install Sol2uml + run: | + npm link sol2uml --only=production + + - name: Set up Python + uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f #v5.1.1 + with: + python-version: '3.8' + + - name: Install solc-select and solc + uses: ./.github/actions/setup-solc-select + with: + to_install: '0.8.19' + to_use: '0.8.19' + + - name: Install Slither + uses: ./.github/actions/setup-slither + + - name: Generate UML + shell: bash + run: | + contract_list="${{ needs.changes.outputs.product_files }}" + + # modify remappings so that solc can find dependencies + ./contracts/scripts/ci/modify_remappings.sh contracts contracts/remappings.txt + mv remappings_modified.txt remappings.txt + + ./contracts/scripts/ci/generate_uml.sh "./" "contracts/uml-diagrams" "$contract_list" + + - name: Generate Slither Markdown reports + run: | + contract_list="${{ needs.changes.outputs.product_files }}" + + echo "::debug::Processing contracts: $contract_list" + ./contracts/scripts/ci/generate_slither_report.sh "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/" contracts/configs/slither/.slither.config-artifacts.json "." "$contract_list" "contracts/slither-reports" "--solc-remaps @=contracts/node_modules/@" + + - name: Upload UMLs and Slither reports + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + timeout-minutes: 10 + continue-on-error: true + with: + name: tmp-contracts-artifacts + path: | + contracts/uml-diagrams + contracts/slither-reports + retention-days: 7 + + - name: Validate if all Slither run for all contracts + uses: ./.github/actions/validate-solidity-artifacts + with: + validate_slither_reports: 'true' + validate_uml_diagrams: 'true' + slither_reports_path: 'contracts/slither-reports' + uml_diagrams_path: 'contracts/uml-diagrams' + sol_files: ${{ needs.changes.outputs.product_files }} + + gather-all-artifacts: + name: Gather all artifacts + if: ${{ needs.changes.outputs.product_changes == 'true' }} + runs-on: ubuntu-latest + needs: [coverage-and-book, uml-static-analysis, gather-basic-info, changes] + steps: + - name: Download all artifacts + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + path: review_artifacts + merge-multiple: true + + - name: Upload all artifacts as single package + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 + with: + name: review-artifacts-${{ inputs.product }}-${{ github.sha }} + path: review_artifacts + retention-days: 60 + + - name: Remove temporary artifacts + uses: geekyeggo/delete-artifact@24928e75e6e6590170563b8ddae9fac674508aa1 # v5.0 + with: + name: tmp-* + + - name: Print Artifact URL in job summary + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ARTIFACTS=$(gh api -X GET repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts) + ARTIFACT_ID=$(echo "$ARTIFACTS" | jq '.artifacts[] | select(.name=="review-artifacts-${{ inputs.product }}-${{ github.sha }}") | .id') + echo "Artifact ID: $ARTIFACT_ID" + + echo "# Solidity Review Artifact Generated" >> $GITHUB_STEP_SUMMARY + echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "[Artifact URL](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts/$ARTIFACT_ID)" >> $GITHUB_STEP_SUMMARY + + notify-no-changes: + if: ${{ needs.changes.outputs.product_changes == 'false' }} + needs: [changes] + runs-on: ubuntu-latest + steps: + - name: Print warning in job summary + shell: bash + run: | + echo "# Solidity Review Artifact NOT Generated" >> $GITHUB_STEP_SUMMARY + echo "Base Ref used: **${{ inputs.base_ref }}**" >> $GITHUB_STEP_SUMMARY + echo "Commit SHA used: **${{ github.sha }}**" >> $GITHUB_STEP_SUMMARY + echo "## Reason: No modified Solidity files found for ${{ inputs.product }}" >> $GITHUB_STEP_SUMMARY + echo "* no modified Solidity files found between ${{ inputs.base_ref }} and ${{ github.sha }} commits" >> $GITHUB_STEP_SUMMARY + echo "* or they are located outside of ./contracts/src/v0.8 folder" >> $GITHUB_STEP_SUMMARY + echo "* or they were limited to test files" >> $GITHUB_STEP_SUMMARY + exit 1 diff --git a/.github/workflows/solidity-hardhat.yml b/.github/workflows/solidity-hardhat.yml index f28cf499072..705fad3be60 100644 --- a/.github/workflows/solidity-hardhat.yml +++ b/.github/workflows/solidity-hardhat.yml @@ -84,4 +84,4 @@ jobs: basic-auth: ${{ secrets.GRAFANA_INTERNAL_BASIC_AUTH }} hostname: ${{ secrets.GRAFANA_INTERNAL_HOST }} this-job-name: Solidity - continue-on-error: true \ No newline at end of file + continue-on-error: true diff --git a/contracts/configs/slither/.slither.config-artifacts.json b/contracts/configs/slither/.slither.config-artifacts.json new file mode 100644 index 00000000000..75071341f51 --- /dev/null +++ b/contracts/configs/slither/.slither.config-artifacts.json @@ -0,0 +1,3 @@ +{ + "filter_paths": "(openzeppelin|mocks/|test/|tests/|testhelpers)" +} diff --git a/contracts/scripts/ci/generate_slither_report.sh b/contracts/scripts/ci/generate_slither_report.sh index e1c526604d1..7fe31d40efc 100755 --- a/contracts/scripts/ci/generate_slither_report.sh +++ b/contracts/scripts/ci/generate_slither_report.sh @@ -1,12 +1,14 @@ #!/usr/bin/env bash +set -euo pipefail + function check_chainlink_dir() { local param_dir="chainlink" current_dir=$(pwd) current_base=$(basename "$current_dir") - if [ "$current_base" != "$param_dir" ]; then + if [[ "$current_base" != "$param_dir" ]]; then >&2 echo "The script must be run from the root of $param_dir directory" exit 1 fi @@ -17,7 +19,7 @@ check_chainlink_dir if [ "$#" -lt 5 ]; then >&2 echo "Generates Markdown Slither reports and saves them to a target directory." >&2 echo "Usage: $0 [slither extra params]" -exit 1 + exit 1 fi REPO_URL=$1 @@ -31,30 +33,34 @@ run_slither() { local FILE=$1 local TARGET_DIR=$2 - # needed, because the action we use returns all modified files, also deleted ones and we must skip those - if [ ! -f "$FILE" ]; then - echo "Warning: File not found: $FILE" - echo "Skipping..." - return + if [[ ! -f "$FILE" ]]; then + >&2 echo "::error:File not found: $FILE" + return 1 fi + set +e source ./contracts/scripts/ci/select_solc_version.sh "$FILE" - if [ $? -ne 0 ]; then - >&2 echo "Error: Failed to select Solc version for $FILE" - exit 1 + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to select Solc version for $FILE" + return 1 fi + set -e SLITHER_OUTPUT_FILE="$TARGET_DIR/$(basename "${FILE%.sol}")-slither-report.md" output=$(slither --config-file "$CONFIG_FILE" "$FILE" --checklist --markdown-root "$REPO_URL" --fail-none $SLITHER_EXTRA_PARAMS) if [ $? -ne 0 ]; then - >&2 echo "Slither failed for $FILE" + >&2 echo "::error::Slither failed for $FILE" exit 1 fi output=$(echo "$output" | sed '/\*\*THIS CHECKLIST IS NOT COMPLETE\*\*. Use `--show-ignored-findings` to show all the results./d' | sed '/Summary/d') echo "# Summary for $FILE" > "$SLITHER_OUTPUT_FILE" echo "$output" >> "$SLITHER_OUTPUT_FILE" + + if [[ -z "$output" ]]; then + echo "No issues found." >> "$SLITHER_OUTPUT_FILE" + fi } process_files() { @@ -70,10 +76,11 @@ process_files() { done } +set +e process_files "$SOURCE_DIR" "$TARGET_DIR" "${FILES[@]}" -if [ $? -ne 0 ]; then - >&2 echo "Error: Failed to generate Slither reports" +if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate Slither reports" exit 1 fi diff --git a/contracts/scripts/ci/generate_uml.sh b/contracts/scripts/ci/generate_uml.sh new file mode 100755 index 00000000000..65745c93bbe --- /dev/null +++ b/contracts/scripts/ci/generate_uml.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function check_chainlink_dir() { + local param_dir="chainlink" + current_dir=$(pwd) + + current_base=$(basename "$current_dir") + + if [[ "$current_base" != "$param_dir" ]]; then + >&2 echo "The script must be run from the root of $param_dir directory" + exit 1 + fi +} + +check_chainlink_dir + +if [ "$#" -lt 2 ]; then + >&2 echo "Generates UML diagrams for all contracts in a directory after flattening them to avoid call stack overflows." + >&2 echo "Usage: $0 [comma-separated list of files]" + exit 1 +fi + +SOURCE_DIR="$1" +TARGET_DIR="$2" +FILES=${3// /} # Remove any spaces from the list of files +FAILED_FILES=() + +flatten_and_generate_uml() { + local FILE=$1 + local TARGET_DIR=$2 + + set +e + FLATTENED_FILE="$TARGET_DIR/flattened_$(basename "$FILE")" + echo "::debug::Flattening $FILE to $FLATTENED_FILE" + forge flatten "$FILE" -o "$FLATTENED_FILE" --root contracts + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to flatten $FILE" + FAILED_FILES+=("$FILE") + return + fi + + OUTPUT_FILE=${FLATTENED_FILE//"flattened_"/""} + OUTPUT_FILE_SVG="${OUTPUT_FILE%.sol}.svg" + echo "::debug::Generating SVG UML for $FLATTENED_FILE to $OUTPUT_FILE_SVG" + sol2uml "$FLATTENED_FILE" -o "$OUTPUT_FILE_SVG" + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate UML diagram in SVG format for $FILE" + FAILED_FILES+=("$FILE") + rm "$FLATTENED_FILE" + return + fi + OUTPUT_FILE_DOT="${OUTPUT_FILE%.sol}.dot" + echo "::debug::Generating DOT UML for $FLATTENED_FILE to $OUTPUT_FILE_DOT" + sol2uml "$FLATTENED_FILE" -o "$OUTPUT_FILE_DOT" -f dot + if [[ $? -ne 0 ]]; then + >&2 echo "::error::Failed to generate UML diagram in DOT format for $FILE" + FAILED_FILES+=("$FILE") + rm "$FLATTENED_FILE" + return + fi + + rm "$FLATTENED_FILE" + set -e +} + +process_all_files_in_directory() { + local SOURCE_DIR=$1 + local TARGET_DIR=$2 + + mkdir -p "$TARGET_DIR" + + find "$SOURCE_DIR" -type f -name '*.sol' | while read -r ITEM; do + flatten_and_generate_uml "$ITEM" "$TARGET_DIR" + done +} + +process_selected_files() { + local SOURCE_DIR=$1 + local TARGET_DIR=$2 + local FILES=(${3//,/ }) # Split the comma-separated list into an array + + mkdir -p "$TARGET_DIR" + + for FILE in "${FILES[@]}"; do + FILE=${FILE//\"/} + MATCHES=($(find "$SOURCE_DIR" -type f -path "*/$FILE")) + + if [[ ${#MATCHES[@]} -gt 1 ]]; then + >&2 echo "Error: Multiple matches found for $FILE:" + for MATCH in "${MATCHES[@]}"; do + >&2 echo " $MATCH" + done + exit 1 + elif [[ ${#MATCHES[@]} -eq 1 ]]; then + >&2 echo "::debug::File found: ${MATCHES[0]}" + flatten_and_generate_uml "${MATCHES[0]}" "$TARGET_DIR" + else + >&2 echo "::error::File $FILE does not exist within the source directory $SOURCE_DIR." + exit 1 + fi + done +} + +# if FILES is empty, process all files in the directory, otherwise process only the selected files +if [[ -z "$FILES" ]]; then + process_all_files_in_directory "$SOURCE_DIR" "$TARGET_DIR" +else + process_selected_files "$SOURCE_DIR" "$TARGET_DIR" "$FILES" +fi + +if [[ "${#FAILED_FILES[@]}" -gt 0 ]]; then + >&2 echo ":error::Failed to generate UML diagrams for ${#FAILED_FILES[@]} files:" + for FILE in "${FAILED_FILES[@]}"; do + >&2 echo " $FILE" + echo "$FILE" >> "$TARGET_DIR/uml_generation_failures.txt" + done +fi + +echo "UML diagrams saved in $TARGET_DIR folder" diff --git a/contracts/scripts/ci/modify_remappings.sh b/contracts/scripts/ci/modify_remappings.sh index a6736f966a7..e64ca369b0c 100755 --- a/contracts/scripts/ci/modify_remappings.sh +++ b/contracts/scripts/ci/modify_remappings.sh @@ -11,7 +11,7 @@ DIR_PREFIX=$1 REMAPPINGS_FILE=$2 if [ ! -f "$REMAPPINGS_FILE" ]; then - >&2 echo "Error: Remappings file '$REMAPPINGS_FILE' not found." + >&2 echo "::error:: Remappings file '$REMAPPINGS_FILE' not found." exit 1 fi diff --git a/contracts/scripts/ci/select_solc_version.sh b/contracts/scripts/ci/select_solc_version.sh index 057d2a29979..3f7d7864ab7 100755 --- a/contracts/scripts/ci/select_solc_version.sh +++ b/contracts/scripts/ci/select_solc_version.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash +set -euo pipefail + function check_chainlink_dir() { local param_dir="chainlink" current_dir=$(pwd) current_base=$(basename "$current_dir") - if [ "$current_base" != "$param_dir" ]; then - echo "The script must be run from the root of $param_dir directory" + if [[ "$current_base" != "$param_dir" ]]; then + >&2 echo "::error::The script must be run from the root of $param_dir directory" exit 1 fi } @@ -16,7 +18,7 @@ check_chainlink_dir FILE="$1" -if [ "$#" -lt 1 ]; then +if [[ "$#" -lt 1 ]]; then echo "Detects the Solidity version of a file and selects the appropriate Solc version." echo "If the version is not installed, it will be installed and selected." echo "Will prefer to use the version from Foundry profile if it satisfies the version in the file." @@ -24,8 +26,8 @@ if [ "$#" -lt 1 ]; then exit 1 fi -if [ -z "$FILE" ]; then - echo "Error: File not provided." +if [[ -z "$FILE" ]]; then + >&2 echo "::error:: File not provided." exit 1 fi @@ -41,7 +43,7 @@ extract_pragma() { if [[ -f "$FILE" ]]; then SOLCVER="$(grep --no-filename '^pragma solidity' "$FILE" | cut -d' ' -f3)" else - echo "$FILE is not a file or it could not be found. Exiting." + >&2 echo ":error::$FILE is not a file or it could not be found. Exiting." return 1 fi SOLCVER="$(echo "$SOLCVER" | sed 's/[^0-9\.^]//g')" @@ -60,14 +62,16 @@ SOLC_IN_PROFILE=$(forge config --json --root contracts | jq ".solc") SOLC_IN_PROFILE=$(echo "$SOLC_IN_PROFILE" | tr -d "'\"") echo "::debug::Detected Solidity version in profile: $SOLC_IN_PROFILE" +set +e SOLCVER=$(extract_pragma "$FILE") -exit_code=$? -if [ $exit_code -ne 0 ]; then +if [[ $? -ne 0 ]]; then echo "Error: Failed to extract the Solidity version from $FILE." return 1 fi +set -e + SOLCVER=$(echo "$SOLCVER" | tr -d "'\"") if [[ "$SOLC_IN_PROFILE" != "null" && -n "$SOLCVER" ]]; then @@ -83,13 +87,13 @@ if [[ "$SOLC_IN_PROFILE" != "null" && -n "$SOLCVER" ]]; then SOLC_TO_USE="$SOLCVER" fi elif [[ "$SOLC_IN_PROFILE" != "null" && -z "$SOLCVER" ]]; then - >&2 echo "No version found in the Solidity file. Exiting" + >&2 echo "::error::No version found in the Solidity file. Exiting" return 1 elif [[ "$SOLC_IN_PROFILE" == "null" && -n "$SOLCVER" ]]; then echo "::debug::Using the version from the file: $SOLCVER" SOLC_TO_USE="$SOLCVER" else - >&2 echo "No version found in the profile or the Solidity file." + >&2 echo "::error::No version found in the profile or the Solidity file." return 1 fi