Massive overhaul of our CI/CD pipeline scripts to be much less brittle and much more durable and reliable, as well as simpler and easier to read and understand. #1
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 | ||
|
Check failure on line 1 in .github/workflows/_ci.yml
|
||
| 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" | ||
| jobs: | ||
| ## --- Jobs to run on every workflow trigger -------------------------------- | ||
| # Runs when: | ||
| # - this workflow is called manually | ||
| # - PR is opened or new commits pushed | ||
| # - PR added to merge queue | ||
| # - release workflow is triggered | ||
| # | ||
| ## --- Global Variables Setup --------------------------------------------- | ||
| # Bridges global env variables to the jobs context so they are safely | ||
| # accessible by downstream reusable workflows. | ||
| setup-env: | ||
| name: "Setup: Global Variables" | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| test-archive-file: ${{ steps.set-env.outputs.test_archive_file }} | ||
| steps: | ||
| - name: Export Env Variables | ||
| id: set-env | ||
| run: echo "test_archive_file=${{ env.TEST_ARCHIVE_FILE }}" >> $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: ubuntu-latest | ||
| 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 ------------------------------------------------ | ||
| # - Check for correct formatting | ||
| # - Required to pass for all subsequent jobs | ||
| check-rustfmt: | ||
| name: "Check: Rust Formatting" | ||
| needs: | ||
| - check-changelog | ||
| runs-on: ubuntu-latest | ||
| 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: >- | ||
| !cancelled() && | ||
| github.event.repository.visibility == 'public' && | ||
| needs.rustfmt.result == 'success' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| node_tag: ${{ steps.check-release.outputs.node_tag }} | ||
| signer_tag: ${{ steps.check-release.outputs.signer_tag }} | ||
| is_node_release: ${{ steps.check-release.outputs.is_node_release }} | ||
| is_signer_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 | ||
| ## --- Setup reusable caches for tests ------------------------------------ | ||
| setup-test-caches: | ||
| name: "Setup: Test Caches" | ||
| needs: | ||
| - check-changelog | ||
| - check-release | ||
| - check-rustfmt | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| uses: ./.github/workflows/setup-test-caches.yml | ||
| with: | ||
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | ||
| ## --- Run stacks core tests ------------------------------------------------ | ||
| tests-stacks-core: | ||
| name: "Tests: Stacks Core" | ||
| needs: | ||
| - check-changelog | ||
| - check-release | ||
| - check-rustfmt | ||
| - setup-test-caches | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| 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-caches | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| 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-caches | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| uses: ./.github/workflows/tests-bitcoin-rpc.yml | ||
| with: | ||
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | ||
| ## --- Run P2P Tests -------------------------------------------------------- | ||
| tests-p2p: | ||
| name: "Tests: P2P" | ||
| needs: | ||
| - check-changelog | ||
| - check-rustfmt | ||
| - check-release | ||
| - setup-caches | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true' || | ||
| github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| uses: ./.github/workflows/tests-p2p.yml | ||
| with: | ||
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | ||
| ## --- Jobs to run on PR or manual trigger ---------------------------------- | ||
| # Runs when: | ||
| # - this workflow is called manually | ||
| # - PR is opened or new commits pushed | ||
| # - PR added to merge queue | ||
| # | ||
| ## --- Cargo Hack check------------------------------------------------------ | ||
| check-cargo-hack: | ||
| name: "Check: Cargo Hack" | ||
| needs: | ||
| - check-changelog | ||
| - check-release | ||
| - check-rustfmt | ||
| if: >- | ||
| !cancelled() && | ||
| (github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| 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 | ||
| if: >- | ||
| !cancelled() && | ||
| (github.event_name == 'workflow_dispatch' || | ||
| github.event_name == 'pull_request' || | ||
| github.event_name == 'merge_group') | ||
| uses: ./.github/workflows/check-constants.yml | ||
| ## --- Jobs to run on release trigger --------------------------------------- | ||
| # Runs when: | ||
| # - it is a node release run | ||
| # | ||
| # --- 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: >- | ||
| !cancelled() && | ||
| github.event.repository.visibility == 'public' && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_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 ---------------------------------------------------------- | ||
| tests-epoch: | ||
| name: "Tests: Epoch" | ||
| needs: | ||
| - check-changelog | ||
| - check-rustfmt | ||
| - check-release | ||
| - setup-caches | ||
| - setup-env | ||
| if: >- | ||
| !cancelled() && | ||
| (needs.check-release.outputs.is_node_release == 'true' || | ||
| needs.check-release.outputs.is_signer_release == 'true') | ||
| uses: ./.github/workflows/tests-epoch.yml | ||
| with: | ||
| archive-file: ${{ needs.setup-env.outputs.test-archive-file }} | ||
| ## Merge and upload code coverage report files once all tests are done | ||
| ## | ||
| ## Runs when: | ||
| ## - always (unless workflow is cancelled OR either blockchain or core tests are skipped) | ||
| create-code-coverage-report: | ||
| if: >- | ||
| always() && | ||
| !cancelled() && | ||
| !contains(needs.stacks-core-tests.result, 'skipped') && | ||
| !contains(needs.bitcoin-tests.result, 'skipped') && | ||
| github.event.repository.visibility == 'public' | ||
| name: "Create: Code Coverage Report" | ||
| runs-on: ubuntu-latest | ||
| needs: | ||
| - 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: | ||
| # 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=() | ||
| 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" & | ||
| done | ||
| # 3. Wait for all background processes to finish | ||
| wait | ||
| # 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 | ||