CI: [7214/merge] #36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # The main Github Actions workflow | |
| name: CI | |
| run-name: "CI: [${{ github.ref_name }}]" | |
| on: | |
| merge_group: | |
| types: | |
| - checks_requested | |
| workflow_dispatch: | |
| pull_request: | |
| types: | |
| - opened | |
| - reopened | |
| - synchronize | |
| # Set default permissions | |
| permissions: | |
| contents: read # default for all jobs | |
| # Set default shell | |
| defaults: | |
| run: | |
| shell: bash | |
| # Configure concurrency | |
| concurrency: | |
| group: ci-${{ github.head_ref || github.ref || github.run_id }} | |
| # Always cancel duplicate jobs | |
| cancel-in-progress: true | |
| env: | |
| TEST_ARCHIVE_FILE: "~/test_archive.tar.zst" | |
| BTC_VERSION: "25.0" | |
| jobs: | |
| # --- Global Variables Setup --------------------------------------------- | |
| # Bridges global env variables to the jobs context so they are safely | |
| # accessible by downstream reusable workflows. | |
| # Also define default runs-on anchor. | |
| setup-env: | |
| name: "Setup: Global Variables" | |
| runs-on: &shared_config ubuntu-latest | |
| outputs: | |
| test-archive-file: ${{ steps.set-env.outputs.test_archive_file }} | |
| btc-version: ${{ steps.set-env.outputs.btc_version }} | |
| job-should-run: ${{ steps.set-env.outputs.job_should_run }} | |
| steps: | |
| - name: Export Env Variables | |
| id: set-env | |
| run: | | |
| # Capture the archive file input, then expand the leading ~ to $HOME | |
| INPUT_FILE="${{ env.TEST_ARCHIVE_FILE }}" | |
| ARCHIVE_FILE="${INPUT_FILE/#\~/$HOME}" | |
| echo "test_archive_file=$ARCHIVE_FILE" >> $GITHUB_OUTPUT | |
| echo "btc_version=${{ env.BTC_VERSION }}" >> $GITHUB_OUTPUT | |
| # Check that evaluates if various jobs should run. Generally we care if it's a workflow dispatch, PR, or merge group. | |
| echo "job_should_run=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group' }}" >> $GITHUB_OUTPUT | |
| # --- Changelog check ------------------------------------------------------ | |
| # Checks for changelog updates (or the 'No Changelog' label) on a PR | |
| # Required to pass for all subsequent jobs | |
| check-changelog: | |
| name: "Check: Changelog" | |
| runs-on: *shared_config | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| steps: | |
| # Checkout the code | |
| # - only .github is required for this job | |
| - name: Checkout the latest code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.ref }} | |
| sparse-checkout: | | |
| .github | |
| # Run script to detect changelog entries | |
| - name: Check for changelog fragments | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | |
| with: | |
| script: | | |
| const changelog = require('./.github/scripts/changelog.js'); | |
| await changelog({ github, context, core }); | |
| # --- Check Rust Formatting ------------------------------------------------ | |
| # Checks for correct formatting | |
| # Required to pass for all subsequent jobs | |
| check-rustfmt: | |
| name: "Check: Rust Formatting" | |
| needs: | |
| - check-changelog | |
| runs-on: *shared_config | |
| steps: | |
| - name: Checkout the latest code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.ref }} | |
| # Setup the rust toolchain with specified rustfmt | |
| - name: Setup Rust Toolchain | |
| uses: ./.github/actions/setup-rust-toolchain | |
| with: | |
| components: rustfmt | |
| # Check Rust formatting | |
| - name: Rustfmt | |
| run: bash ./.github/scripts/rustfmt.sh | |
| # --- Check if this is a release build ------------------------------------- | |
| # Outputs: | |
| # - node_tag: Tag of the stacks-node if the branch is a release one (example: release/3.4.0.0.1), null otherwise | |
| # - signer_tag: Tag of the stacks-signer if the branch is a release one (example: release/3.4.0.0.1.0), null otherwise | |
| # - is_node_release: True if the branch represents a 'stacks-node' release, false otherwise. | |
| # - If this is true, 'is_signer_release' will also be true, since a 'stacks-signer' binary is always released alongside 'stacks-node'. | |
| # - is_signer_release: True if the branch represents a 'stacks-signer' release, false otherwise. | |
| check-release: | |
| name: "Check: Release" | |
| needs: | |
| - check-changelog | |
| - check-rustfmt | |
| if: >- | |
| github.event.repository.visibility == 'public' | |
| runs-on: *shared_config | |
| outputs: | |
| is_release: ${{ steps.check-release.outputs.is_node_release || steps.check-release.outputs.is_signer_release }} | |
| steps: | |
| # Checkout the code | |
| # - only .github is required for this job | |
| - name: Checkout the latest code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.ref }} | |
| sparse-checkout: | | |
| .github | |
| # Run script to check if this is a release and set outputs | |
| - name: Check Release | |
| id: check-release | |
| env: | |
| BRANCH: ${{ github.ref_name }} | |
| run: bash ./.github/scripts/check_release.sh | |
| # --- Cargo Hack check------------------------------------------------------ | |
| check-cargo-hack: | |
| name: "Check: Cargo Hack" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| - setup-env | |
| if: >- | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/check-cargo-hack.yml | |
| # --- Validate constants dumped by stacks-inspect -------------------------- | |
| check-constants: | |
| name: "Check: Constants" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| - setup-env | |
| if: >- | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/check-constants.yml | |
| # --- Setup reusable caches for tests ------------------------------------ | |
| setup-test-caches: | |
| name: "Setup: Test Caches" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' || | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/setup-test-caches.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| btc-version: ${{ needs.setup-env.outputs.btc-version }} | |
| # --- Run stacks core tests ------------------------------------------------ | |
| tests-stacks-core: | |
| name: "Tests: Stacks Core" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| - setup-test-caches | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' || | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/tests-stacks-core.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| # --- Run bitcoin Tests ---------------------------------------------------- | |
| tests-bitcoin: | |
| name: "Tests: Bitcoin" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| - setup-test-caches | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' || | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/tests-bitcoin.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| # --- Run bitcoin RPC Tests ------------------------------------------------ | |
| tests-bitcoin-rpc: | |
| name: "Tests: Bitcoin RPC" | |
| needs: | |
| - check-changelog | |
| - check-rustfmt | |
| - check-release | |
| - setup-test-caches | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' || | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/tests-bitcoin-rpc.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| btc-version: ${{ needs.setup-env.outputs.btc-version }} | |
| # --- Run P2P Tests -------------------------------------------------------- | |
| tests-p2p: | |
| name: "Tests: P2P" | |
| needs: | |
| - check-changelog | |
| - check-rustfmt | |
| - check-release | |
| - setup-test-caches | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' || | |
| needs.setup-env.outputs.job-should-run == 'true' | |
| uses: ./.github/workflows/tests-p2p.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| # --- Create a release --------------------------------------- | |
| # Creates binary archives for several architectures | |
| # Creates Docker images and pushes to ghcr registry | |
| # Creates a draft GitHub release | |
| create-release: | |
| name: "Create: Release" | |
| needs: | |
| - check-changelog | |
| - check-release | |
| - check-rustfmt | |
| if: >- | |
| github.event.repository.visibility == 'public' && | |
| needs.check-release.outputs.is_release == 'true' | |
| permissions: | |
| contents: write # required for github release | |
| id-token: write # required for attestation | |
| attestations: write # required for attestation | |
| packages: write # required for image push to ghcr | |
| uses: ./.github/workflows/release-github.yml | |
| with: | |
| node_tag: ${{ needs.check-release.outputs.node_tag }} # 5 place version format like 1.2.3.4.5 | |
| signer_tag: ${{ needs.check-release.outputs.signer_tag }} # 6 place version format like 1.2.3.4.5.6 | |
| is_node_release: ${{ needs.check-release.outputs.is_node_release }} # boolean used in matrix conditional in .github/workflows/release-github.yml | |
| # --- Epoch Tests ---------------------------------------------------------- | |
| # Only run for releases | |
| tests-epoch: | |
| name: "Tests: Epoch" | |
| needs: | |
| - check-changelog | |
| - check-rustfmt | |
| - check-release | |
| - setup-test-caches | |
| - setup-env | |
| if: >- | |
| needs.check-release.outputs.is_release == 'true' | |
| uses: ./.github/workflows/tests-epoch.yml | |
| with: | |
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | |
| # --- Create Code Coverage Report ------------------------------------------- | |
| create-code-coverage-report: | |
| if: >- | |
| always() && | |
| github.event.repository.visibility == 'public' | |
| name: "Create: Code Coverage Report" | |
| runs-on: *shared_config | |
| needs: | |
| - check-changelog | |
| - check-rustfmt | |
| - check-release | |
| - tests-bitcoin | |
| - tests-epoch | |
| - tests-p2p | |
| - tests-stacks-core | |
| env: | |
| REPORT_FILES_DIR: "code_coverage_files" | |
| REPORT_FILES_EXT: "info" | |
| REPORT_MERGE_NUM_THREADS: 4 | |
| steps: | |
| # Fail this job explicitly if any upstream job fails | |
| - name: Check for upstream failures | |
| if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') | |
| shell: bash | |
| run: | | |
| echo "ERROR: One or more required CI jobs failed or were cancelled. Failing coverage report." | |
| exit 1 | |
| # Checkout the code (Coveralls requires source code to be available when action is called) | |
| - name: Checkout the latest code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # Download the code coverage .info files generated by tests from artifacts (prefixed by commit SHA) | |
| - name: Download code coverage artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1 | |
| with: | |
| pattern: ${{ github.sha }}-*.${{ env.REPORT_FILES_EXT }} | |
| path: ${{ env.REPORT_FILES_DIR }} | |
| merge-multiple: true | |
| # Check for at least 1 code coverage files | |
| - name: Check for at least 1 code coverage file | |
| shell: bash | |
| run: | | |
| EXT="${{ env.REPORT_FILES_EXT }}" | |
| DIR="${{ env.REPORT_FILES_DIR }}" | |
| file_count=$(find -type f -wholename "./$DIR/*.$EXT" | wc -l) | |
| if [ "$file_count" -eq 0 ]; then | |
| echo "ERROR: no code coverage files of type .$EXT found to merge. Verify that they were correctly generated and uploaded in prior CI steps" | |
| exit 1 | |
| fi | |
| # Install lcov for merging the reports | |
| - name: Install lcov | |
| shell: bash | |
| run: | | |
| sudo apt-get install -y --no-install-recommends lcov | |
| # Merge n coverage report files into 1 file using lcov | |
| - name: Merge code coverage files | |
| shell: bash | |
| run: | | |
| EXT="${{ env.REPORT_FILES_EXT }}" | |
| DIR="${{ env.REPORT_FILES_DIR }}" | |
| INTERMEDIATE_FILES=() | |
| PIDS=() # Array to track background process IDs | |
| cd "$DIR" || exit 1 | |
| # 1. Collect all files into an array | |
| mapfile -d '' ALL_FILES < <(find . -type f -name "*.$EXT" -print0) | |
| # 2. Process in chunks (Dynamically calculated for 4 threads) | |
| TOTAL_FILES=${#ALL_FILES[@]} | |
| CHUNK_SIZE=$(( (TOTAL_FILES + 3) / $REPORT_MERGE_NUM_THREADS )) | |
| # Guardrail: Ensure CHUNK_SIZE is at least 1 to prevent infinite loops | |
| if [ "$CHUNK_SIZE" -le 0 ]; then | |
| CHUNK_SIZE=1 | |
| fi | |
| echo "Total files to process: $TOTAL_FILES. Calculated chunk size for $REPORT_MERGE_NUM_THREADS threads: $CHUNK_SIZE" | |
| for (( i=0; i<TOTAL_FILES; i+=CHUNK_SIZE )); do | |
| # Slice the array for the current chunk | |
| CHUNK=("${ALL_FILES[@]:i:CHUNK_SIZE}") | |
| # Build the lcov arguments for this specific chunk | |
| CHUNK_ARGS=() | |
| for file in "${CHUNK[@]}"; do | |
| CHUNK_ARGS+=(-a "$file") | |
| done | |
| # Define a unique name for the intermediate report | |
| PART_FILE="code_coverage_part_$((i / CHUNK_SIZE)).$EXT" | |
| INTERMEDIATE_FILES+=("$PART_FILE") | |
| # Run lcov in the background | |
| echo "Processing chunk $((i / CHUNK_SIZE + 1))..." | |
| lcov "${CHUNK_ARGS[@]}" -o "$PART_FILE" & | |
| PIDS+=("$!") # Capture the PID of the process just put in the background | |
| done | |
| # 3. Wait for all background processes to finish and catch errors | |
| echo "Waiting for ${#PIDS[@]} background jobs to complete..." | |
| FAILED=0 | |
| for pid in "${PIDS[@]}"; do | |
| if ! wait "$pid"; then | |
| echo "ERROR: Background process with PID $pid failed." | |
| FAILED=1 | |
| fi | |
| done | |
| if [ "$FAILED" -ne 0 ]; then | |
| echo "ERROR: One or more lcov chunk operations failed. Aborting final merge." | |
| exit 1 | |
| fi | |
| # 4. Final Merge: Combine the intermediate files into the final report | |
| FINAL_ARGS=() | |
| for part in "${INTERMEDIATE_FILES[@]}"; do | |
| FINAL_ARGS+=(-a "$part") | |
| done | |
| echo "Performing final merge of ${#INTERMEDIATE_FILES[@]} intermediate files..." | |
| lcov "${FINAL_ARGS[@]}" -o "code-coverage-report.$EXT" | |
| cd .. | |
| # Upload the merged code coverage file to Coveralls | |
| - name: Upload code coverage to Coveralls | |
| uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 | |
| with: | |
| file: ${{ env.REPORT_FILES_DIR }}/code-coverage-report.${{ env.REPORT_FILES_EXT }} | |
| compare-ref: ${{ github.base_ref }} # defaults to master if this isn't supplied | |
| build-number: ${{ github.run_id }}-${{ github.run_attempt }} # include run attempt in build so that repeated CI job runs don't cause a "job already closed" error on upload | |
| fail-on-error: true |