Repository: Consensys/maru
Pull request #116: feat: support using maru container in e2e tests
Author: login: jonesho
Created: 2025-05-23T10:21:19Z | Merged: 2025-05-27T13:40:05Z

This PR implements #74 for running the "unit and e2e tests" and "docker container smoke test" in parallel and if the workflow is "triggered manually" or from "push request to main ", the image will be pushed right after the smoke test was passed (skipped the check of "unit and e2e tests")


Diff:
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 53b78ad24..affcd6c62 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -42,6 +42,10 @@ on:
       DOCKERHUB_TOKEN:
         required: false
 
+concurrency:
+  group: run-e2e-tests-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
 jobs:
   # Required job
   run-e2e-tests:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5a4d60e38..4715fbc35 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -48,21 +48,32 @@ jobs:
   run-e2e-tests:
     needs: [ filter-commit-changes ]
     if: ${{ always() && !cancelled() && needs.filter-commit-changes.outputs.has-changes-requiring-build == 'true' }}
-    concurrency:
-      group: run-e2e-tests-${{ github.workflow }}-${{ github.ref }}
-      cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
     uses: ./.github/workflows/e2e-tests.yml
     with:
       e2e-tests-logs-dump: true
       has-changes-requiring-build: ${{ needs.filter-commit-changes.outputs.has-changes-requiring-build }}
     secrets: inherit
 
+  build-for-testing:
+    if: ${{ always() && !cancelled() }}
+    uses: ./.github/workflows/maru-build-and-publish.yml
+    with:
+      develop_tag: 'develop'
+      image_name: 'consensys/maru'
+      push_image: false
+
+  maru-image-smoke-test:
+    needs: [ build-for-testing ]
+    uses: ./.github/workflows/smoke-tests.yml
+    with:
+      commit-tag: ${{ needs.build-for-testing.outputs.commit_tag }}
+
   build-and-publish:
-    needs: [ testing, run-e2e-tests ]
-    if: ${{ always() && !cancelled() && (needs.testing.result == 'skipped' || needs.testing.result == 'success') && (needs.run-e2e-tests.result == 'skipped' || needs.run-e2e-tests.outputs.tests_outcome == 'success') }}
+    needs: [ maru-image-smoke-test ]
+    if: ${{ always() && !cancelled() && needs.maru-image-smoke-test.result == 'success' && (github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main') }}
     uses: ./.github/workflows/maru-build-and-publish.yml
     with:
       develop_tag: 'develop'
       image_name: 'consensys/maru'
-      push_image: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }}
-    secrets: inherit
+      push_image: true
+    secrets: inherit
\ No newline at end of file
diff --git a/.github/workflows/maru-build-and-publish.yml b/.github/workflows/maru-build-and-publish.yml
index 225360bdd..eb8331c6b 100644
--- a/.github/workflows/maru-build-and-publish.yml
+++ b/.github/workflows/maru-build-and-publish.yml
@@ -17,6 +17,9 @@ on:
         required: false
         type: boolean
         default: false
+    outputs:
+      commit_tag:
+        value: ${{ jobs.build-and-publish.outputs.commit_tag }}
     secrets:
       DOCKERHUB_USERNAME:
         required: false
@@ -68,6 +71,8 @@ jobs:
       TAGS: ${{ inputs.image_name }}:${{ inputs.commit_tag }}
       DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
       DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
+    outputs:
+      commit_tag: ${{ env.COMMIT_TAG }}
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -82,8 +87,7 @@ jobs:
           fi
           echo "COMMIT_TAG=$COMMIT_TAG" >> $GITHUB_ENV
           echo "TAGS=${{ env.IMAGE_NAME }}:$COMMIT_TAG" >> $GITHUB_ENV
-          echo "COMMIT_TAG: ${{ env.COMMIT_TAG }} $COMMIT_TAG"
-          echo "TAGS: ${{ env.TAGS }}"
+          echo "COMMIT_TAG=$COMMIT_TAG"
       - name: Set develop tag if main branch
         if: ${{ github.ref == 'refs/heads/main' }}
         run: |
diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml
new file mode 100644
index 000000000..24a3ce995
--- /dev/null
+++ b/.github/workflows/smoke-tests.yml
@@ -0,0 +1,53 @@
+name: Smoke Tests CI
+
+on:
+  workflow_call:
+    inputs:
+     commit-tag:
+       required: true
+       type: string
+
+concurrency:
+  group: run-smoke-tests-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+  run-smoke-tests:
+    runs-on: [gha-runner-scale-set-ubuntu-22.04-amd64-med]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Download local docker image artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: linea-maru
+      - name: Load Docker images
+        run: |
+          pwd && ls -la && echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" &&
+          gunzip -c $GITHUB_WORKSPACE/linea-maru/linea-maru-docker-image.tar.gz | docker load
+      - name: Run Docker with maru container
+        run: |
+          MARU_TAG=${{ inputs.commit-tag }} make run-local-image-partial
+      - name: List docker containers/images
+        continue-on-error: true
+        run: |
+          docker ps -la && docker images
+          docker container ls -a
+      - name: Check Maru Docker logs
+        run: |
+          SEARCH_STRING="Maru is up"
+          TIMEOUT_SECONDS=30
+          INTERVAL_SECONDS=2
+          END_TIME=$((SECONDS + TIMEOUT_SECONDS))
+          while [ $SECONDS -lt $END_TIME ]; do
+            echo $(docker logs maru | grep -q "Maru is up")
+            LOGS=$(docker logs maru 2>&1)
+            if echo "$LOGS" | grep -q "$SEARCH_STRING"; then
+              echo "Found '$SEARCH_STRING' in logs. Marking job as successful."
+              exit 0
+            fi
+            echo "String '$SEARCH_STRING' not found yet. Retrying in $INTERVAL_SECONDS seconds..."
+            sleep $INTERVAL_SECONDS
+          done
+          echo "Timeout of $TIMEOUT_SECONDS seconds reached. String '$SEARCH_STRING' not found in logs."
+          exit 1
diff --git a/Makefile b/Makefile
index 4ddae65bb..925744a33 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,9 @@ pre-commit:
 run-e2e-test:
 	./gradlew acceptanceTest
 
+run-e2e-test-use-maru-container:
+	./gradlew acceptanceTest -PuseMaruContainer=true
+
 clean:
 	./gradlew clean
 
@@ -21,4 +24,7 @@ build-local-image:
 	docker build app --build-context=libs=./app/build/install/app/lib/ --build-context=maru=./app/build/libs/ -t local/maru:latest
 
 run-local-image:
-	docker compose -f docker/compose.yaml -f docker/compose.dev.yaml up -d
\ No newline at end of file
+	docker compose -f docker/compose.yaml -f docker/compose.dev.yaml up -d
+
+run-local-image-partial:
+	docker compose -f docker/compose.yaml -f docker/compose.dev.yaml up -d maru
diff --git a/docker/compose.dev.yaml b/docker/compose.dev.yaml
index 445b02d11..55dd23e4a 100644
--- a/docker/compose.dev.yaml
+++ b/docker/compose.dev.yaml
@@ -1,4 +1,4 @@
-#docker compose -f docker/compose.yml -f docker/compose.dev.overrides.yml
+#docker compose -f docker/compose.yml -f docker/compose.dev.yml
 
 services:
 
@@ -7,7 +7,7 @@ services:
     # docker build app --build-context=libs=./app/build/install/app/lib/ --build-context=maru=./app/build/libs/ -t local/maru:latest
     # command: [ 'java', '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005','-Dvertx.configurationFile=/path/to/vertx-options.json', '-Dlog4j2.configurationFile=/path/to/log4j2-dev.xml']
     #image: local/maru:latest
-    image: consensys/maru:${MARU_TAG:-0bd171f}
+    image: consensys/maru:${MARU_TAG:-2687246}
     container_name: maru
     depends_on:
       - sequencer
@@ -15,12 +15,11 @@ services:
     command: [ 'java', '-jar', "maru.jar", '--besu-genesis-file', 'configs/genesis.json', '--config', 'configs/config.dev.toml' ]
     volumes:
       - ./maru/config.dev.toml:/opt/consensys/maru/configs/config.dev.toml:ro
-      - ./maru/genesis.json:/opt/consensys/maru/configs/genesis.json:ro
-      - ./maru/key:/tmp/maru-db/private-key
+      - ./initialization/genesis-maru.json:/opt/consensys/maru/configs/genesis.json:ro
+      - ./maru/0x1b9abeec3215d8ade8a33607f2cf0f4f60e5f0d0.key:/tmp/maru-db/private-key
       - ./jwt:/opt/consensys/docker/jwt:ro
     ports:
       # Attach the IDE's remote java debugger to localhost:5005 to debug maru
       - "5005:5005"
     networks:
-      - linea
-
+      - linea
\ No newline at end of file
diff --git a/docker/initialization/genesis-maru.json.template b/docker/initialization/genesis-maru.json.template
new file mode 100644
index 000000000..cbfd0df58
--- /dev/null
+++ b/docker/initialization/genesis-maru.json.template
@@ -0,0 +1,15 @@
+{
+  "config": {
+    "0": {
+      "type": "delegated",
+      "blockTimeSeconds": 1
+    },
+    "%PRAGUE_TIME%": {
+      "type": "qbft",
+      "validatorSet": ["0x1b9abeec3215d8ade8a33607f2cf0f4f60e5f0d0"],
+      "blockTimeSeconds": 7,
+      "feeRecipient": "0x0000000000000000000000000000000000000000",
+      "elFork": "Prague"
+    }
+  }
+}
diff --git a/docker/initialization/init.sh b/docker/initialization/init.sh
index ffd457b50..4201bd149 100755
--- a/docker/initialization/init.sh
+++ b/docker/initialization/init.sh
@@ -1,16 +1,18 @@
 #!/bin/zsh
 echo "Checking if genesis files already exist."
 
-if [[ ! -f "initialization/genesis-besu.json" && ! -f "initialization/genesis-geth.json" && ! -f "initialization/genesis-nethermind.json" ]]; then
-    echo "Initialization of timestamp in genesis files for Besu, Geth, and Nethermind."
+if [[ ! -f "initialization/genesis-besu.json" && ! -f "initialization/genesis-geth.json" && ! -f "initialization/genesis-nethermind.json"  && ! -f "initialization/genesis-maru.json" ]]; then
+    echo "Initialization of timestamp in genesis files for Maru, Besu, Geth, and Nethermind."
     date
     cd initialization || exit
+    cp -T "genesis-maru.json.template" "genesis-maru.json"
     cp -T "genesis-besu.json.template" "genesis-besu.json"
     cp -T "genesis-geth.json.template" "genesis-geth.json"
     cp -T "genesis-nethermind.json.template" "genesis-nethermind.json"
 
     merge_timestamp=$(($(date +%s) + 60))
     echo "Timestamp: $merge_timestamp"
+    sed -i "s/%PRAGUE_TIME%/$merge_timestamp/g" genesis-maru.json
     sed -i "s/%SWITCH_TIME%/$merge_timestamp/g" genesis-besu.json
     sed -i "s/%SWITCH_TIME%/$merge_timestamp/g" genesis-geth.json
     merge_timestamp_hex=$(printf "0x%x" $merge_timestamp)
diff --git a/docker/maru/key b/docker/maru/0x1b9abeec3215d8ade8a33607f2cf0f4f60e5f0d0.key
similarity index 100%
rename from docker/maru/key
rename to docker/maru/0x1b9abeec3215d8ade8a33607f2cf0f4f60e5f0d0.key
diff --git a/docker/maru/config.dev.toml b/docker/maru/config.dev.toml
index 227a08f05..34bdf30eb 100644
--- a/docker/maru/config.dev.toml
+++ b/docker/maru/config.dev.toml
@@ -1,8 +1,6 @@
 [persistence]
 data-path="/tmp/maru-db"
-
-[sot-eth-endpoint]
-endpoint = "http://sequencer:8545"
+private-key-path="/tmp/maru-db/private-key"
 
 [qbft-options]
 communication-margin=100m
@@ -11,10 +9,9 @@ communication-margin=100m
 port = 3322
 ip-address = "127.0.0.1"
 
-[validator]
-private-key = "0x1dd171cec7e2995408b5513004e8207fe88d6820aeff0d82463b3e41df251aae"
-min-time-between-get-payload-attempts=6000m
-el-client-engine-api-endpoint = "http://sequencer:8550"
+[payload-validator]
+engine-api-endpoint = { endpoint = "http://sequencer:8550" }
+eth-api-endpoint = { endpoint = "http://sequencer:8545" }
 
 [follower-engine-apis]
 "follower-besu" = { endpoint = "http://follower-besu:8550" }
diff --git a/e2e/build.gradle b/e2e/build.gradle
index 6326f1e6a..965d16962 100644
--- a/e2e/build.gradle
+++ b/e2e/build.gradle
@@ -72,4 +72,5 @@ tasks.register('acceptanceTest', Test) { test ->
   }
 
   systemProperties["junit.jupiter.execution.parallel.enabled"] = false
+  systemProperty("useMaruContainer", project.findProperty("useMaruContainer"))
 }
diff --git a/e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt b/e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
index 271cedb4f..ea371adcd 100644
--- a/e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
+++ b/e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
@@ -17,6 +17,7 @@ package maru.e2e
 
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.palantir.docker.compose.DockerComposeRule
+import com.palantir.docker.compose.configuration.DockerComposeFiles
 import com.palantir.docker.compose.configuration.ProjectName
 import com.palantir.docker.compose.connection.waiting.HealthChecks
 import java.io.File
@@ -60,10 +61,17 @@ import tech.pegasys.teku.infrastructure.async.SafeFuture
 
 class CliqueToPosTest {
   companion object {
+    private val useMaruContainer: Boolean = System.getProperty("useMaruContainer").toBoolean()
+    private val dockerComposeFilePaths =
+      mutableListOf<String>(Path.of("./../docker/compose.yaml").toString()).also {
+        if (useMaruContainer) {
+          it.add(Path.of("./../docker/compose.dev.yaml").toString())
+        }
+      }
     private val qbftCluster =
       DockerComposeRule
         .Builder()
-        .file(Path.of("./../docker/compose.yaml").toString())
+        .files(DockerComposeFiles.from(*dockerComposeFilePaths.toTypedArray()))
         .projectName(ProjectName.random())
         .waitingForService("sequencer", HealthChecks.toHaveAllPortsOpen())
         .build()
@@ -72,6 +80,7 @@ class CliqueToPosTest {
     private val genesisDir = File("../docker/initialization")
     private val dataDir = File("/tmp/maru-db").also { it.deleteOnExit() }
     private val transactionsHelper = Web3jTransactionsHelper(TestEnvironment.sequencerL2Client)
+    private val log: Logger = LogManager.getLogger(this::javaClass)
 
     private fun parsePragueSwitchTimestamp(): Long {
       val objectMapper = ObjectMapper()
@@ -81,24 +90,30 @@ class CliqueToPosTest {
     }
 
     private fun deleteGenesisFiles() {
+      val maruGenesis = File(genesisDir, "genesis-maru.json")
       val besuGenesis = File(genesisDir, "genesis-besu.json")
       val gethGenesis = File(genesisDir, "genesis-geth.json")
       val nethermindGenesis = File(genesisDir, "genesis-nethermind.json")
+      maruGenesis.delete()
       besuGenesis.delete()
       gethGenesis.delete()
       nethermindGenesis.delete()
     }
 
+    private fun containerShortNameToFullId(containerShortName: String) = containerShortName
+
     @BeforeAll
     @JvmStatic
     fun beforeAll() {
+      log.info("Using Maru as docker container: $useMaruContainer")
       deleteGenesisFiles()
       dataDir.deleteRecursively()
+      dataDir.mkdirs()
       qbftCluster.before()
-
       pragueSwitchTimestamp = parsePragueSwitchTimestamp()
-      dataDir.mkdirs()
-      maru = MaruFactory.buildTestMaru(pragueSwitchTimestamp)
+      if (!useMaruContainer) {
+        maru = MaruFactory.buildTestMaru(pragueSwitchTimestamp)
+      }
     }
 
     @AfterAll
@@ -113,8 +128,12 @@ class CliqueToPosTest {
         )
       }
 
-      TestEnvironment.allClients.forEach {
-        val containerShortName = it.key
+      val containerShortNames =
+        TestEnvironment.allClients
+          .map { it.key }
+          .toMutableList()
+          .also { if (useMaruContainer) it.add("maru") }
+      containerShortNames.forEach { containerShortName ->
         qbftCluster
           .dockerExecutable()
           .execute("logs", containerShortNameToFullId(containerShortName))
@@ -131,11 +150,6 @@ class CliqueToPosTest {
       qbftCluster.after()
     }
 
-    private fun containerShortNameToFullId(containerShortName: String) =
-      "${qbftCluster.projectName().asString()}-$containerShortName"
-
-    private val log: Logger = LogManager.getLogger(CliqueToPosTest::class.java)
-
     @JvmStatic
     fun followerNodes(): List<Arguments> =
       TestEnvironment.followerExecutionClientsPostMerge.map {
@@ -146,7 +160,9 @@ class CliqueToPosTest {
   @Order(1)
   @Test
   fun networkCanBeSwitched() {
-    maru.start()
+    if (!useMaruContainer) {
+      maru.start()
+    }
     sendCliqueTransactions()
     everyoneArePeered()
     waitTillTimestamp(pragueSwitchTimestamp)
@@ -161,7 +177,9 @@ class CliqueToPosTest {
     assertNodeBlockHeight(TestEnvironment.sequencerL2Client)
 
     waitForAllBlockHeightsToMatch()
-    maru.stop()
+    if (!useMaruContainer) {
+      maru.stop()
+    }
   }
 
   // TODO: Explore parallelization of this test
@@ -189,6 +207,18 @@ class CliqueToPosTest {
           .timeout(30.seconds.toJavaDuration())
       }
 
+    if (nodeName.contains("besu")) {
+      // Required to change validation rules from Clique to PostMerge
+      // TODO: investigate this issue more. It was working happen with Dummy Consensus
+      syncTarget(engineApiConfig, 5)
+      awaitCondition
+        .ignoreExceptions()
+        .alias(nodeName)
+        .untilAsserted {
+          assertNodeBlockHeight(nodeEthereumClient, 5L)
+        }
+    }
+
     awaitCondition
       .ignoreExceptions()
       .alias(nodeName)


Reviews:
user: login: Filter94
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:30:48Z
user: login: Filter94
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:31:18Z
user: login: Filter94
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:31:58Z
user: login: Filter94
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:33:40Z
user: login: Filter94
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:36:44Z
user: login: Filter94
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-27T08:37:33Z
user: login: jonesho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T11:08:33Z
user: login: Filter94
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-27T12:33:19Z

Review comments:
diff_hunk: @@ -81,24 +90,30 @@ class CliqueToPosTest {
     }
 
     private fun deleteGenesisFiles() {
+      val maruGenesis = File(genesisDir, "genesis-maru.json")
       val besuGenesis = File(genesisDir, "genesis-besu.json")
       val gethGenesis = File(genesisDir, "genesis-geth.json")
       val nethermindGenesis = File(genesisDir, "genesis-nethermind.json")
+      maruGenesis.delete()
       besuGenesis.delete()
       gethGenesis.delete()
       nethermindGenesis.delete()
     }
 
+    private fun containerShortNameToFullId(containerShortName: String) = containerShortName
+
     @BeforeAll
     @JvmStatic
     fun beforeAll() {
+      log.info("Using Maru as docker container: $useMaruContainer")
       deleteGenesisFiles()
       dataDir.deleteRecursively()
+      dataDir.mkdirs()
       qbftCluster.before()
-
       pragueSwitchTimestamp = parsePragueSwitchTimestamp()
-      dataDir.mkdirs()
-      maru = MaruFactory.buildTestMaru(pragueSwitchTimestamp)
+      if (!useMaruContainer) {
path: e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
user: login: Filter94
body: Minor: a slightly cleaner way to do it would be to define a parent abstract class with the tests themselves and have 2 separate classes. Only one of which would be active based on `useMaruContainer`

It's a quite complex refactoring though and I don't think it provides enough value ATM, so I'm fine with the way things are
created_at: 2025-05-27T08:30:48Z
author_association: COLLABORATOR
diff_hunk: @@ -113,8 +128,15 @@ class CliqueToPosTest {
         )
       }
 
-      TestEnvironment.allClients.forEach {
-        val containerShortName = it.key
+      val containerShortNames =
+        TestEnvironment.allClients
+          .map {
+            it.key
+          }.toMutableList()
+          .also {
+            it.add("maru")
path: e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
user: login: Filter94
body: I believe this should be conditional
created_at: 2025-05-27T08:31:18Z
author_association: COLLABORATOR
diff_hunk: @@ -72,6 +80,7 @@ class CliqueToPosTest {
     private val genesisDir = File("../docker/initialization")
     private val dataDir = File("/tmp/maru-db").also { it.deleteOnExit() }
     private val transactionsHelper = Web3jTransactionsHelper(TestEnvironment.sequencerL2Client)
+    private val log: Logger = LogManager.getLogger(CliqueToPosTest::class.java)
path: e2e/src/acceptanceTest/kotlin/maru/e2e/CliqueToPosTest.kt
user: login: Filter94
body: ```suggestion
    private val log: Logger = LogManager.getLogger(this::javaClass)
```
created_at: 2025-05-27T08:31:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,53 @@
+name: Smoke Tests CI
+
+on:
+  workflow_call:
+    inputs:
+     commit-tag:
+       required: true
+       type: string
+
+concurrency:
+  group: run-smoke-tests-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+  run-smoke-tests:
+    runs-on: [gha-runner-scale-set-ubuntu-22.04-amd64-med]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Download local docker image artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: linea-maru
+      - name: Load Docker images
+        run: |
+          pwd && ls -la && echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" &&
+          gunzip -c $GITHUB_WORKSPACE/linea-maru/linea-maru-docker-image.tar.gz | docker load
+      - name: Run Docker with maru container
+        run: |
+          MARU_TAG=${{ inputs.commit-tag }} make run-local-image-partial
+      - name: List docker containers/images
+        continue-on-error: true
+        run: |
+          docker ps -la && docker images
+          docker container ls -a
+      - name: Check Maru Docker logs
+        run: |
+          SEARCH_STRING="Maru is up"
+          TIMEOUT_SECONDS=30
+          INTERVAL_SECONDS=2
+          END_TIME=$((SECONDS + TIMEOUT_SECONDS))
+          while [ $SECONDS -lt $END_TIME ]; do
+            echo $(docker logs maru | grep -q "Maru is up")
+            LOGS=$(docker logs maru 2>&1)
+            if echo "$LOGS" | grep -q "$SEARCH_STRING"; then
+              echo "Found '$SEARCH_STRING' in logs. Marking job as successful."
+              exit 0
+            fi
+            echo "String '$SEARCH_STRING' not found yet. Retrying in $INTERVAL_SECONDS seconds..."
+            sleep $INTERVAL_SECONDS
+          done
+          echo "Timeout of $TIMEOUT_SECONDS seconds reached. String '$SEARCH_STRING' not found in logs."
path: .github/workflows/smoke-tests.yml
user: login: Filter94
body: Minor: This could be done by GHA's timeout-minutes. I wonder if there's a support for seconds though. Maybe 1 minute is too long
created_at: 2025-05-27T08:33:40Z
author_association: COLLABORATOR
diff_hunk: @@ -48,21 +48,34 @@ jobs:
   run-e2e-tests:
     needs: [ filter-commit-changes ]
     if: ${{ always() && !cancelled() && needs.filter-commit-changes.outputs.has-changes-requiring-build == 'true' }}
-    concurrency:
-      group: run-e2e-tests-${{ github.workflow }}-${{ github.ref }}
-      cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
     uses: ./.github/workflows/e2e-tests.yml
     with:
       e2e-tests-logs-dump: true
       has-changes-requiring-build: ${{ needs.filter-commit-changes.outputs.has-changes-requiring-build }}
     secrets: inherit
 
+  build-for-testing:
+    #needs: [ testing, run-e2e-tests ]
+    #if: ${{ always() && !cancelled() && (needs.testing.result == 'skipped' || needs.testing.result == 'success') && (needs.run-e2e-tests.result == 'skipped' || needs.run-e2e-tests.outputs.tests_outcome == 'success') }}
path: .github/workflows/main.yml
user: login: Filter94
body: Can we remove these comments?
created_at: 2025-05-27T08:36:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,53 @@
+name: Smoke Tests CI
+
+on:
+  workflow_call:
+    inputs:
+     commit-tag:
+       required: true
+       type: string
+
+concurrency:
+  group: run-smoke-tests-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+
+jobs:
+  run-smoke-tests:
+    runs-on: [gha-runner-scale-set-ubuntu-22.04-amd64-med]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Download local docker image artifacts
+        uses: actions/download-artifact@v4
+        with:
+          pattern: linea-maru
+      - name: Load Docker images
+        run: |
+          pwd && ls -la && echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE" &&
+          gunzip -c $GITHUB_WORKSPACE/linea-maru/linea-maru-docker-image.tar.gz | docker load
+      - name: Run Docker with maru container
+        run: |
+          MARU_TAG=${{ inputs.commit-tag }} make run-local-image-partial
+      - name: List docker containers/images
+        continue-on-error: true
+        run: |
+          docker ps -la && docker images
+          docker container ls -a
+      - name: Check Maru Docker logs
+        run: |
+          SEARCH_STRING="Maru is up"
+          TIMEOUT_SECONDS=30
+          INTERVAL_SECONDS=2
+          END_TIME=$((SECONDS + TIMEOUT_SECONDS))
+          while [ $SECONDS -lt $END_TIME ]; do
+            echo $(docker logs maru | grep -q "Maru is up")
+            LOGS=$(docker logs maru 2>&1)
+            if echo "$LOGS" | grep -q "$SEARCH_STRING"; then
+              echo "Found '$SEARCH_STRING' in logs. Marking job as successful."
+              exit 0
+            fi
+            echo "String '$SEARCH_STRING' not found yet. Retrying in $INTERVAL_SECONDS seconds..."
+            sleep $INTERVAL_SECONDS
+          done
+          echo "Timeout of $TIMEOUT_SECONDS seconds reached. String '$SEARCH_STRING' not found in logs."
path: .github/workflows/smoke-tests.yml
user: login: jonesho
body: It seems there's no GHA built-in for `timeout-seconds`, I'd keep it as is for now
created_at: 2025-05-27T11:08:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: Filter94
created_at: 2025-05-23T12:14:09Z
body: I don't think this solution serves the purpose. What we want to cover is:
* E2E test
* Maru image smoke test

We don't want to run all the E2E tests with Maru in Docker, because it's less efficient on CI and it's harder to debug locally.

For the image tests we don't really need the same coverage as we have for E2E tests and even if we have this option, we don't want to run the same tests for Maru in Docker and Maru in process on CI for economical reasons.

I think just a check if the container  doesn't fail with given config + assertion on container's logs should be sufficient

author_association: COLLABORATOR
user: login: jframe
created_at: 2025-05-26T00:21:35Z
body: @jonesho Is PR to this fix this issue https://github.com/Consensys/maru/issues/74?
author_association: CONTRIBUTOR
user: login: jonesho
created_at: 2025-05-26T04:17:38Z
body: > @jonesho Is PR to this fix this issue #74?

@jframe Yes, and I'm working on the comment from Roman now 
author_association: CONTRIBUTOR
user: login: jonesho
created_at: 2025-05-26T10:17:43Z
body: > For the image tests we don't really need the same coverage as we have for E2E tests and even if we have this option, we don't want to run the same tests for Maru in Docker and Maru in process on CI for economical reasons.

@Filter94 initially I was thinking about replacing the current e2e tests (using Maru in process) with the one using Maru container, such that we don't even need a smoke test, but I'm also keen on keeping the in-process e2e tests and adding a smoke test for the Maru container, if debugging is in our better interest

>  I think just a check if the container doesn't fail with given config + assertion on container's logs should be sufficient

Any suggested string to asset from the Maru container log? Would "Maru is up" be good enough?
author_association: CONTRIBUTOR
user: login: Filter94
created_at: 2025-05-26T10:20:02Z
body: > > For the image tests we don't really need the same coverage as we have for E2E tests and even if we have this option, we don't want to run the same tests for Maru in Docker and Maru in process on CI for economical reasons.
> 
> @Filter94 initially I was thinking about replacing the current e2e tests (using Maru in process) with the one using Maru container, such that we don't even need a smoke test, but I'm also keen on keeping the in-process e2e tests and adding a smoke test for the Maru container, if debugging is in our better interest
> 
> > I think just a check if the container doesn't fail with given config + assertion on container's logs should be sufficient
> 
> Any suggested string to asset from the Maru container log? Would "Maru is up" be good enough?

Yes, I think "Maru is up" is good enough. In the future we might check that it does something by using metrics endpoint or even beacon chain JsonRPC API, but both aren't implemented yet
author_association: COLLABORATOR