diff --git a/.github/workflows/release-binaries.yaml b/.github/workflows/release-binaries.yaml new file mode 100644 index 0000000..15ad43e --- /dev/null +++ b/.github/workflows/release-binaries.yaml @@ -0,0 +1,226 @@ +name: Release Binaries + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to build and release (e.g. v0.6.0)' + required: true + type: string + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + CARGO_NET_GIT_FETCH_WITH_CLI: "true" + +concurrency: + group: release-binaries-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + os: linux + runner: ${{ vars.RELEASE_RUNNER_LINUX_AMD64 || 'ubuntu-24.04' }} + - target: aarch64-unknown-linux-gnu + os: linux + runner: ${{ vars.RELEASE_RUNNER_LINUX_ARM64 || 'ubuntu-24.04-arm' }} + - target: aarch64-apple-darwin + os: macos + runner: ${{ vars.RELEASE_RUNNER_MACOS_ARM64 || 'macos-15' }} + steps: + - name: Resolve artifact version + id: version + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + VERSION="$INPUT_TAG" + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then + VERSION="${GITHUB_REF#refs/tags/}" + else + echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" + exit 1 + fi + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then + echo "::error::Invalid tag format: '$VERSION' (expected e.g. v1.2.3 or v1.2.3-rc.1)" + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }} + submodules: recursive + + - name: Install Linux system dependencies + if: matrix.os == 'linux' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libclang-dev + + - name: Install macOS system dependencies + if: matrix.os == 'macos' + run: | + brew install llvm pkg-config + LLVM_PREFIX="$(brew --prefix llvm)" + echo "${LLVM_PREFIX}/bin" >> "$GITHUB_PATH" + echo "LIBCLANG_PATH=${LLVM_PREFIX}/lib" >> "$GITHUB_ENV" + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 + + - name: Validate runner target + env: + TARGET: ${{ matrix.target }} + run: | + HOST="$(rustc -vV | awk '/^host:/ {print $2}')" + if [[ "$HOST" != "$TARGET" ]]; then + echo "::error::Runner host '$HOST' does not match release target '$TARGET'" + exit 1 + fi + + - name: Install sccache + uses: taiki-e/install-action@a661f9d0b5f5222ce08202e6b2e9648d1713ca37 # untagged 2025-07-01 commit + with: + tool: sccache + + - name: Configure sccache + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Build release binaries + run: | + cargo build --locked --release \ + --bin arc-node-execution \ + --bin arc-node-consensus \ + --bin arc-snapshots + env: + RUSTC_WRAPPER: sccache + SCCACHE_GHA_ENABLED: "true" + + - name: Package release assets + env: + TAG: ${{ steps.version.outputs.version }} + TARGET: ${{ matrix.target }} + run: ./scripts/release-package.sh "$TAG" "$TARGET" + + - name: Upload build artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-${{ matrix.target }} + path: release-assets/ + + sign: + name: Prepare Release Assets + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + env: + HAS_RELEASE_GPG_KEY: ${{ secrets.RELEASE_GPG_PRIVATE_KEY != '' }} + steps: + - name: Download build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: release-* + merge-multiple: true + path: release-assets/ + + - name: Import GPG key + if: env.HAS_RELEASE_GPG_KEY == 'true' + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 + with: + gpg_private_key: ${{ secrets.RELEASE_GPG_PRIVATE_KEY }} + + - name: GPG sign archives + if: env.HAS_RELEASE_GPG_KEY == 'true' + run: | + for archive in release-assets/*.tar.gz; do + gpg --batch --yes --detach-sign --armor --output "${archive}.asc" "${archive}" + done + + - name: Upload release assets + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: release-assets + path: release-assets/ + + release: + name: Create Release + needs: sign + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Resolve tag + id: tag + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ inputs.tag }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + TAG="$INPUT_TAG" + elif [[ "$GITHUB_REF" == refs/tags/* ]]; then + TAG="${GITHUB_REF#refs/tags/}" + else + echo "::error::Unsupported release trigger: $EVENT_NAME $GITHUB_REF" + exit 1 + fi + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then + echo "::error::Invalid tag format: '$TAG' (expected e.g. v1.2.3 or v1.2.3-rc.1)" + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+- ]]; then + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" + else + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + fi + + - name: Download build artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: release-assets + path: release-assets/ + + - name: Upload release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ steps.tag.outputs.tag }} + IS_PRERELEASE: ${{ steps.tag.outputs.is_prerelease }} + run: | + create_args=(--draft --generate-notes) + edit_args=(--draft=false) + if [[ "$IS_PRERELEASE" == "true" ]]; then + create_args+=(--prerelease) + edit_args+=(--prerelease=true) + fi + + if gh release view "${TAG}" >/dev/null 2>&1; then + echo "::error::Release ${TAG} already exists; delete the existing draft/release before rerunning" + exit 1 + fi + + gh release create "${TAG}" "${create_args[@]}" + gh release upload "${TAG}" release-assets/* + gh release edit "${TAG}" "${edit_args[@]}"