From ab9060dabe23af907599b2a787db208abdc9241c Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 27 Mar 2026 12:55:18 +0800 Subject: [PATCH 1/6] ci: add Docker multi-platform build and Release workflows Add production Docker and release infrastructure: - Dockerfile: cargo-chef multi-stage build with debian:bookworm-slim runtime, supports release/maxperf profiles via BUILD_PROFILE arg - .dockerignore: excludes target, .git, tests, local-test - docker.yml: tag-triggered multi-platform (amd64+arm64) build, pushes to ghcr.io/morph-l2/morph-reth with semver tags - release.yml: tag-triggered binary release with version validation, cross-compiled Linux binaries (x86_64+aarch64), SHA256 checksums, auto-generated changelog, and GitHub draft release --- .dockerignore | 13 +++ .github/workflows/docker.yml | 56 ++++++++++++ .github/workflows/release.yml | 157 ++++++++++++++++++++++++++++++++++ Dockerfile | 46 ++++++++++ 4 files changed, 272 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/release.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..223d0b6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +target/ +.git/ +.github/ +.claude/ +.worktrees/ +.config/ +local-test/ +tests/ +*.md +deny.toml +typos.toml +.editorconfig +.gitignore diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..ce8ced6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + IMAGE_NAME: ghcr.io/morph-l2/morph-reth + +jobs: + build-push: + name: Build and Push + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..26e2c29 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,157 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g., v0.1.0)" + required: true + dry_run: + description: "Dry run (skip release creation)" + type: boolean + default: true + +permissions: + contents: write + +env: + CARGO_TERM_COLOR: always + +jobs: + extract-version: + name: Extract Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Determine tag + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + TAG="${{ inputs.tag }}" + else + TAG="${GITHUB_REF#refs/tags/}" + fi + VERSION="${TAG#v}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Extracted version: $VERSION" + + check-version: + name: Check Version + runs-on: ubuntu-latest + needs: extract-version + if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Verify Cargo.toml version matches tag + run: | + CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps \ + | jq -r '.packages[] | select(.name == "morph-reth") | .version') + TAG_VERSION="${{ needs.extract-version.outputs.version }}" + echo "Cargo.toml version: $CARGO_VERSION" + echo "Tag version: $TAG_VERSION" + if [[ "$TAG_VERSION" != "$CARGO_VERSION"* ]]; then + echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" + exit 1 + fi + + build: + name: Build ${{ matrix.target }} + runs-on: ubuntu-latest + needs: [extract-version, check-version] + if: ${{ !cancelled() && (needs.check-version.result == 'success' || needs.check-version.result == 'skipped') }} + strategy: + matrix: + include: + - target: x86_64-unknown-linux-gnu + archive: morph-reth-${{ needs.extract-version.outputs.version }}-x86_64-linux + - target: aarch64-unknown-linux-gnu + archive: morph-reth-${{ needs.extract-version.outputs.version }}-aarch64-linux + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + key: ${{ matrix.target }} + + - name: Install cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Build binary + run: cross build --release --locked --target ${{ matrix.target }} --bin morph-reth + + - name: Package binary + run: | + mkdir -p dist + cp target/${{ matrix.target }}/release/morph-reth dist/ + cd dist + tar czf ../${{ matrix.archive }}.tar.gz morph-reth + cd .. + shasum -a 256 ${{ matrix.archive }}.tar.gz > ${{ matrix.archive }}.tar.gz.sha256 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.archive }} + path: | + ${{ matrix.archive }}.tar.gz + ${{ matrix.archive }}.tar.gz.sha256 + + draft-release: + name: Draft Release + runs-on: ubuntu-latest + needs: [extract-version, build] + if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: artifacts + + - name: Generate changelog + id: changelog + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") + if [[ -n "$PREVIOUS_TAG" ]]; then + CHANGELOG=$(git log --pretty=format:"- %s" ${PREVIOUS_TAG}..HEAD) + else + CHANGELOG=$(git log --pretty=format:"- %s" HEAD~20..HEAD) + fi + # Write to file to avoid escaping issues + echo "$CHANGELOG" > changelog.md + + - name: Create draft release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ needs.extract-version.outputs.version }}" + PRERELEASE="" + if [[ "$VERSION" == *"alpha"* ]] || [[ "$VERSION" == *"beta"* ]] || [[ "$VERSION" == *"rc"* ]]; then + PRERELEASE="--prerelease" + fi + gh release create "v${VERSION}" \ + --draft \ + --title "v${VERSION}" \ + --notes-file changelog.md \ + $PRERELEASE \ + artifacts/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f77a429 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef +WORKDIR /app + +LABEL org.opencontainers.image.source=https://github.com/morph-l2/morph-reth +LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" + +# reth-mdbx-sys requires libclang for bindgen +RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config + +# Generate dependency recipe +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +# Build dependencies + application +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json + +# Build profile, release by default +ARG BUILD_PROFILE=release +ENV BUILD_PROFILE=$BUILD_PROFILE + +# Extra Cargo flags +ARG RUSTFLAGS="" +ENV RUSTFLAGS="$RUSTFLAGS" + +# Build dependencies (cached layer) +RUN cargo chef cook --profile $BUILD_PROFILE --recipe-path recipe.json + +# Build the application +COPY . . +RUN cargo build --profile $BUILD_PROFILE --locked --bin morph-reth + +# Copy binary to a fixed location (ARG not resolved in COPY) +RUN cp /app/target/$BUILD_PROFILE/morph-reth /app/morph-reth + +# Minimal runtime image +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/morph-reth /usr/local/bin/ + +EXPOSE 8545 8546 8551 30303 30303/udp + +ENTRYPOINT ["/usr/local/bin/morph-reth"] From ae6438e1005b8bf3080005455ecdf87087356501 Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 27 Mar 2026 14:34:23 +0800 Subject: [PATCH 2/6] fix(ci): address code review feedback for Docker and Release workflows - docker.yml: only publish on tag push, not on manual workflow_dispatch - release.yml: thread resolved tag ref through all checkout steps so manual dispatch builds the correct commit - release.yml: use exact version match instead of prefix glob - release.yml: use `git log -n 20` fallback for changelog (safe with <20 commits) - release.yml: add --verify-tag to gh release create - Dockerfile: remove apt-get upgrade for reproducibility, add non-root runtime user --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 14 ++++++++++++-- Dockerfile | 7 ++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ce8ced6..f9ec544 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -48,7 +48,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - push: true + push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }} platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26e2c29..cf57b18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,17 +25,21 @@ jobs: runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} + ref: ${{ steps.version.outputs.ref }} steps: - name: Determine tag id: version run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then TAG="${{ inputs.tag }}" + REF="refs/tags/${TAG}" else TAG="${GITHUB_REF#refs/tags/}" + REF="${GITHUB_REF}" fi VERSION="${TAG#v}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "ref=$REF" >> "$GITHUB_OUTPUT" echo "Extracted version: $VERSION" check-version: @@ -46,6 +50,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ needs.extract-version.outputs.ref }} - name: Verify Cargo.toml version matches tag run: | @@ -54,7 +60,7 @@ jobs: TAG_VERSION="${{ needs.extract-version.outputs.version }}" echo "Cargo.toml version: $CARGO_VERSION" echo "Tag version: $TAG_VERSION" - if [[ "$TAG_VERSION" != "$CARGO_VERSION"* ]]; then + if [[ "$TAG_VERSION" != "$CARGO_VERSION" ]]; then echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" exit 1 fi @@ -74,6 +80,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ needs.extract-version.outputs.ref }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -120,6 +128,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: + ref: ${{ needs.extract-version.outputs.ref }} fetch-depth: 0 - name: Download all artifacts @@ -135,7 +144,7 @@ jobs: if [[ -n "$PREVIOUS_TAG" ]]; then CHANGELOG=$(git log --pretty=format:"- %s" ${PREVIOUS_TAG}..HEAD) else - CHANGELOG=$(git log --pretty=format:"- %s" HEAD~20..HEAD) + CHANGELOG=$(git log -n 20 --pretty=format:"- %s") fi # Write to file to avoid escaping issues echo "$CHANGELOG" > changelog.md @@ -150,6 +159,7 @@ jobs: PRERELEASE="--prerelease" fi gh release create "v${VERSION}" \ + --verify-tag \ --draft \ --title "v${VERSION}" \ --notes-file changelog.md \ diff --git a/Dockerfile b/Dockerfile index f77a429..74aeedc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ LABEL org.opencontainers.image.source=https://github.com/morph-l2/morph-reth LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" # reth-mdbx-sys requires libclang for bindgen -RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config +RUN apt-get update && \ + apt-get install -y --no-install-recommends libclang-dev pkg-config && \ + rm -rf /var/lib/apt/lists/* # Generate dependency recipe FROM chef AS planner @@ -37,10 +39,13 @@ RUN cp /app/target/$BUILD_PROFILE/morph-reth /app/morph-reth # Minimal runtime image FROM debian:bookworm-slim AS runtime RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ + useradd --system --create-home --home-dir /var/lib/morph-reth --shell /usr/sbin/nologin morph-reth && \ rm -rf /var/lib/apt/lists/* COPY --from=builder /app/morph-reth /usr/local/bin/ EXPOSE 8545 8546 8551 30303 30303/udp +WORKDIR /var/lib/morph-reth +USER morph-reth ENTRYPOINT ["/usr/local/bin/morph-reth"] From 1cf107222f81e38b58b9e73138503d8bcaa09427 Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Fri, 27 Mar 2026 15:07:43 +0800 Subject: [PATCH 3/6] fix(ci): move LABEL to runtime stage, fix latest tag and sha256sum - Move OCI labels from chef stage to runtime stage so they appear on final image - Change latest tag to always apply on tag push (is_default_branch is false for tags) - Use sha256sum instead of shasum for Linux CI runners --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- Dockerfile | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f9ec544..0f38012 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,7 +42,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix= - type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=latest,enable=true - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf57b18..c97376e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,7 +109,7 @@ jobs: cd dist tar czf ../${{ matrix.archive }}.tar.gz morph-reth cd .. - shasum -a 256 ${{ matrix.archive }}.tar.gz > ${{ matrix.archive }}.tar.gz.sha256 + sha256sum ${{ matrix.archive }}.tar.gz > ${{ matrix.archive }}.tar.gz.sha256 - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/Dockerfile b/Dockerfile index 74aeedc..856f8bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,6 @@ FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef WORKDIR /app -LABEL org.opencontainers.image.source=https://github.com/morph-l2/morph-reth -LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" - # reth-mdbx-sys requires libclang for bindgen RUN apt-get update && \ apt-get install -y --no-install-recommends libclang-dev pkg-config && \ @@ -38,6 +35,9 @@ RUN cp /app/target/$BUILD_PROFILE/morph-reth /app/morph-reth # Minimal runtime image FROM debian:bookworm-slim AS runtime + +LABEL org.opencontainers.image.source=https://github.com/morph-l2/morph-reth +LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ useradd --system --create-home --home-dir /var/lib/morph-reth --shell /usr/sbin/nologin morph-reth && \ rm -rf /var/lib/apt/lists/* From c9428a34418dda4ae92f502dc98f97bfffe8f58c Mon Sep 17 00:00:00 2001 From: panos-xyz Date: Fri, 27 Mar 2026 15:08:35 +0800 Subject: [PATCH 4/6] fix(ci): scope write permissions to draft-release and always run version check - Move contents:write to draft-release job only, default to contents:read - Add persist-credentials:false to check-version and build checkouts - Remove dry_run skip on check-version so version is always validated - Require check-version success (not skipped) before build proceeds --- .github/workflows/release.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c97376e..5310d9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ on: default: true permissions: - contents: write + contents: read env: CARGO_TERM_COLOR: always @@ -46,12 +46,12 @@ jobs: name: Check Version runs-on: ubuntu-latest needs: extract-version - if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ needs.extract-version.outputs.ref }} + persist-credentials: false - name: Verify Cargo.toml version matches tag run: | @@ -69,7 +69,7 @@ jobs: name: Build ${{ matrix.target }} runs-on: ubuntu-latest needs: [extract-version, check-version] - if: ${{ !cancelled() && (needs.check-version.result == 'success' || needs.check-version.result == 'skipped') }} + if: ${{ !cancelled() && needs.check-version.result == 'success' }} strategy: matrix: include: @@ -82,6 +82,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.extract-version.outputs.ref }} + persist-credentials: false - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -124,6 +125,8 @@ jobs: runs-on: ubuntu-latest needs: [extract-version, build] if: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }} + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 From 41293cb41aac5d16705ef30f0307c7fcea620ab0 Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 27 Mar 2026 15:20:50 +0800 Subject: [PATCH 5/6] fix(ci): validate manual tag input format in release workflow Reject non-semver inputs (e.g., `0.1.0` without `v` prefix) early in extract-version instead of failing late during release creation. --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5310d9a..4474de4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,10 @@ jobs: run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then TAG="${{ inputs.tag }}" + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Invalid tag format: '$TAG'. Expected format like v0.1.0 or v0.1.0-rc.1" + exit 1 + fi REF="refs/tags/${TAG}" else TAG="${GITHUB_REF#refs/tags/}" From 300a33edf5c574983e4192def74d3d97b9a6ff7c Mon Sep 17 00:00:00 2001 From: panos Date: Fri, 27 Mar 2026 16:14:28 +0800 Subject: [PATCH 6/6] fix(ci): skip latest tag on prerelease and use prefix version match - docker.yml: only tag as `latest` when ref has no `-` (excludes prerelease tags like v0.1.0-rc.1, v0.1.0-alpha) - release.yml: use prefix match for version check so tag v0.1.0-rc.1 passes when Cargo.toml version is 0.1.0 (matches scroll-reth pattern) --- .github/workflows/docker.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0f38012..6cd021a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -42,7 +42,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix= - type=raw,value=latest,enable=true + type=raw,value=latest,enable=${{ !contains(github.ref, '-') }} - name: Build and push uses: docker/build-push-action@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4474de4..9f3a1ba 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,7 @@ jobs: TAG_VERSION="${{ needs.extract-version.outputs.version }}" echo "Cargo.toml version: $CARGO_VERSION" echo "Tag version: $TAG_VERSION" - if [[ "$TAG_VERSION" != "$CARGO_VERSION" ]]; then + if [[ "$TAG_VERSION" != "$CARGO_VERSION"* ]]; then echo "::error::Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" exit 1 fi