From a9872b74b4150c4c1290a593f214f960c57335ad Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sun, 14 Dec 2025 11:50:28 +0100 Subject: [PATCH 1/7] feat: server hooks for lcov and corpus reuse --- .github/workflows/release-fork.yml | 92 ++++++ lib/Echidna.hs | 3 +- lib/Echidna/Output/Source.hs | 24 +- lib/Echidna/Server.hs | 34 +- lib/Echidna/Types/Config.hs | 2 + package.yaml | 2 + tests/solidity/basic/alex-sequence.sol | 411 ++++++++++++++++++++++++ tests/solidity/basic/alex-sequence.yaml | 2 + 8 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release-fork.yml create mode 100644 tests/solidity/basic/alex-sequence.sol create mode 100644 tests/solidity/basic/alex-sequence.yaml diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml new file mode 100644 index 000000000..8d5e624b3 --- /dev/null +++ b/.github/workflows/release-fork.yml @@ -0,0 +1,92 @@ +name: "Build & Release" + +on: + workflow_dispatch: # Manual trigger from GitHub UI + inputs: + version: + description: 'Version name (e.g., 2.3.0-custom)' + required: true + default: 'dev' + push: + tags: + - "v*" + +jobs: + build: + name: Build ${{ matrix.name }} + timeout-minutes: 180 + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + name: Linux (x86_64) + tuple: x86_64-linux + system: x86_64-linux + - os: macos-14 + name: macOS (aarch64) + tuple: aarch64-macos + system: aarch64-darwin + # Uncomment for additional platforms: + # - os: ubuntu-24.04-arm + # name: Linux (aarch64) + # tuple: aarch64-linux + # system: aarch64-linux + # - os: macos-14 + # name: macOS (x86_64) + # tuple: x86_64-macos + # system: x86_64-darwin + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v14 + + - name: Set version + id: version + run: | + if [[ -n "${{ github.event.inputs.version }}" ]]; then + echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.ref }}" =~ ^refs/tags/v.* ]]; then + echo "version=$(echo "${{ github.ref }}" | sed 's#^refs/tags/v##')" >> "$GITHUB_OUTPUT" + else + echo "version=HEAD-$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT" + fi + + - name: Build redistributable echidna + run: | + nix build ".#packages.${{ matrix.system }}.echidna-redistributable" --out-link redistributable + tar -czf "echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz" -C ./redistributable/bin/ echidna + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: echidna-${{ matrix.tuple }} + path: echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz + + release: + name: Create release + needs: [build] + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + pattern: echidna-* + merge-multiple: true + + - name: List artifacts + run: ls -la echidna-*.tar.gz + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + draft: true + name: "Echidna (custom build)" + tag_name: ${{ github.ref_name || format('v{0}', github.event.inputs.version) }} + files: ./echidna-*.tar.gz diff --git a/lib/Echidna.hs b/lib/Echidna.hs index cb0c26187..7e07aa6f2 100644 --- a/lib/Echidna.hs +++ b/lib/Echidna.hs @@ -132,7 +132,8 @@ mkEnv cfg buildOutput tests world slitherInfo = do contractNameCache <- newIORef mempty -- TODO put in real path let dapp = dappInfo "/" buildOutput - pure $ Env { cfg, dapp, codehashMap, fetchSession, contractNameCache + sourceCache = buildOutput.sources + pure $ Env { cfg, dapp, sourceCache, codehashMap, fetchSession, contractNameCache , chainId, eventQueue, coverageRefInit, coverageRefRuntime, corpusRef, testRefs, world , slitherInfo } diff --git a/lib/Echidna/Output/Source.hs b/lib/Echidna/Output/Source.hs index b271b182a..212013b74 100644 --- a/lib/Echidna/Output/Source.hs +++ b/lib/Echidna/Output/Source.hs @@ -1,7 +1,11 @@ {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE TemplateHaskell #-} -module Echidna.Output.Source where +module Echidna.Output.Source ( + saveCoverages, + saveLcovHook, + checkAssertionsCoverage +) where import Prelude hiding (writeFile) @@ -91,6 +95,24 @@ coverageFileExtension Lcov = ".lcov" coverageFileExtension Html = ".html" coverageFileExtension Txt = ".txt" +-- | Save only LCOV coverage triggered by HTTP hook, with timestamp in filename +saveLcovHook + :: Env + -> FilePath + -> SourceCache + -> [SolcContract] + -> IO FilePath +saveLcovHook env d sc cs = do + coverage <- mergeCoverageMaps env.dapp env.coverageRefInit env.coverageRefRuntime + currentTime <- getCurrentTime + let timestamp = formatTime defaultTimeLocale "%Y%m%d_%H%M%S" currentTime + fn = d "hook_" <> timestamp <> ".lcov" + excludePatterns = env.cfg.campaignConf.coverageExcludes + cc = ppCoveredCode Lcov sc cs coverage Nothing (T.pack timestamp) excludePatterns + createDirectoryIfMissing True d + writeFile fn cc + pure fn + -- | Pretty-print the covered code ppCoveredCode :: CoverageFileType -> SourceCache -> [SolcContract] -> FrozenCoverageMap -> Maybe Text -> Text -> [Text] -> Text ppCoveredCode fileType sc cs s projectName timestamp excludePatterns diff --git a/lib/Echidna/Server.hs b/lib/Echidna/Server.hs index 8e3be5851..9f81c8101 100644 --- a/lib/Echidna/Server.hs +++ b/lib/Echidna/Server.hs @@ -5,13 +5,20 @@ import Control.Monad (when, void) import Data.Aeson import Data.Binary.Builder (fromLazyByteString) import Data.IORef +import Data.Map qualified as Map import Data.Time (LocalTime) import Data.Word (Word16) +import Network.HTTP.Types (status200, status404) +import Network.Wai (Application, responseLBS, pathInfo, requestMethod) import Network.Wai.EventSource (ServerEvent(..), eventSourceAppIO) import Network.Wai.Handler.Warp (run) +import EVM.Dapp (DappInfo(..)) + +import Echidna.Output.Source (saveLcovHook) +import Echidna.Types.Campaign (CampaignConf(..)) +import Echidna.Types.Config (Env(..), EConfig(..)) import Echidna.Worker() -import Echidna.Types.Config (Env(..)) import Echidna.Types.Worker newtype SSE = SSE (LocalTime, CampaignEvent) @@ -71,5 +78,28 @@ runSSEServer serverStopVar env port nworkers = do , eventData = [ fromLazyByteString $ encode (SSE event) ] } + let sseApp = eventSourceAppIO sseListener + + let app :: Application + app req respond = case (requestMethod req, pathInfo req) of + -- SSE endpoint + ("GET", ["events"]) -> sseApp req respond + + -- Dump LCOV coverage + ("POST", ["dump_lcov"]) -> do + case env.cfg.campaignConf.corpusDir of + Nothing -> + respond $ responseLBS status404 [("Content-Type", "application/json")] + "{\"error\":\"No corpus directory configured\"}" + Just dir -> do + let contracts = Map.elems env.dapp.solcByName + fn <- saveLcovHook env dir env.sourceCache contracts + respond $ responseLBS status200 [("Content-Type", "application/json")] + (encode $ object ["file" .= fn]) + + -- Unknown endpoint + _ -> respond $ responseLBS status404 [("Content-Type", "application/json")] + "{\"error\":\"Not found\"}" + void . forkIO $ do - run (fromIntegral port) $ eventSourceAppIO sseListener + run (fromIntegral port) app diff --git a/lib/Echidna/Types/Config.hs b/lib/Echidna/Types/Config.hs index a86df54fb..c29c28d91 100644 --- a/lib/Echidna/Types/Config.hs +++ b/lib/Echidna/Types/Config.hs @@ -9,6 +9,7 @@ import Data.Time (LocalTime) import Data.Word (Word64) import EVM.Dapp (DappInfo) +import EVM.Solidity (SourceCache) import EVM.Types (Addr, W256) import EVM.Fetch qualified as Fetch @@ -69,6 +70,7 @@ data EConfigWithUsage = EConfigWithUsage data Env = Env { cfg :: EConfig , dapp :: DappInfo + , sourceCache :: SourceCache -- | Shared between all workers. Events are fairly rare so contention is -- minimal. diff --git a/package.yaml b/package.yaml index 6e519db4b..6882105cb 100644 --- a/package.yaml +++ b/package.yaml @@ -76,8 +76,10 @@ library: - vector - vty - vty-crossplatform + - wai - wai-extra - warp + - http-types - wreq - word-wrap - xml-conduit diff --git a/tests/solidity/basic/alex-sequence.sol b/tests/solidity/basic/alex-sequence.sol new file mode 100644 index 000000000..a61352117 --- /dev/null +++ b/tests/solidity/basic/alex-sequence.sol @@ -0,0 +1,411 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract AlexSequence { + uint256 public state = 0; + + function echidna_opt_state() public view returns (int256) { + return int256(state); + } + + function step0() public { + require(state == 0); + state = 1; + } + function step1() public { + require(state == 1); + state = 2; + } + function step2() public { + require(state == 2); + state = 3; + } + function step3() public { + require(state == 3); + state = 4; + } + function step4() public { + require(state == 4); + state = 5; + } + function step5() public { + require(state == 5); + state = 6; + } + function step6() public { + require(state == 6); + state = 7; + } + function step7() public { + require(state == 7); + state = 8; + } + function step8() public { + require(state == 8); + state = 9; + } + function step9() public { + require(state == 9); + state = 10; + } + function step10() public { + require(state == 10); + state = 11; + } + function step11() public { + require(state == 11); + state = 12; + } + function step12() public { + require(state == 12); + state = 13; + } + function step13() public { + require(state == 13); + state = 14; + } + function step14() public { + require(state == 14); + state = 15; + } + function step15() public { + require(state == 15); + state = 16; + } + function step16() public { + require(state == 16); + state = 17; + } + function step17() public { + require(state == 17); + state = 18; + } + function step18() public { + require(state == 18); + state = 19; + } + function step19() public { + require(state == 19); + state = 20; + } + function step20() public { + require(state == 20); + state = 21; + } + function step21() public { + require(state == 21); + state = 22; + } + function step22() public { + require(state == 22); + state = 23; + } + function step23() public { + require(state == 23); + state = 24; + } + function step24() public { + require(state == 24); + state = 25; + } + function step25() public { + require(state == 25); + state = 26; + } + function step26() public { + require(state == 26); + state = 27; + } + function step27() public { + require(state == 27); + state = 28; + } + function step28() public { + require(state == 28); + state = 29; + } + function step29() public { + require(state == 29); + state = 30; + } + function step30() public { + require(state == 30); + state = 31; + } + function step31() public { + require(state == 31); + state = 32; + } + function step32() public { + require(state == 32); + state = 33; + } + function step33() public { + require(state == 33); + state = 34; + } + function step34() public { + require(state == 34); + state = 35; + } + function step35() public { + require(state == 35); + state = 36; + } + function step36() public { + require(state == 36); + state = 37; + } + function step37() public { + require(state == 37); + state = 38; + } + function step38() public { + require(state == 38); + state = 39; + } + function step39() public { + require(state == 39); + state = 40; + } + function step40() public { + require(state == 40); + state = 41; + } + function step41() public { + require(state == 41); + state = 42; + } + function step42() public { + require(state == 42); + state = 43; + } + function step43() public { + require(state == 43); + state = 44; + } + function step44() public { + require(state == 44); + state = 45; + } + function step45() public { + require(state == 45); + state = 46; + } + function step46() public { + require(state == 46); + state = 47; + } + function step47() public { + require(state == 47); + state = 48; + } + function step48() public { + require(state == 48); + state = 49; + } + function step49() public { + require(state == 49); + state = 50; + } + function step50() public { + require(state == 50); + state = 51; + } + function step51() public { + require(state == 51); + state = 52; + } + function step52() public { + require(state == 52); + state = 53; + } + function step53() public { + require(state == 53); + state = 54; + } + function step54() public { + require(state == 54); + state = 55; + } + function step55() public { + require(state == 55); + state = 56; + } + function step56() public { + require(state == 56); + state = 57; + } + function step57() public { + require(state == 57); + state = 58; + } + function step58() public { + require(state == 58); + state = 59; + } + function step59() public { + require(state == 59); + state = 60; + } + function step60() public { + require(state == 60); + state = 61; + } + function step61() public { + require(state == 61); + state = 62; + } + function step62() public { + require(state == 62); + state = 63; + } + function step63() public { + require(state == 63); + state = 64; + } + function step64() public { + require(state == 64); + state = 65; + } + function step65() public { + require(state == 65); + state = 66; + } + function step66() public { + require(state == 66); + state = 67; + } + function step67() public { + require(state == 67); + state = 68; + } + function step68() public { + require(state == 68); + state = 69; + } + function step69() public { + require(state == 69); + state = 70; + } + function step70() public { + require(state == 70); + state = 71; + } + function step71() public { + require(state == 71); + state = 72; + } + function step72() public { + require(state == 72); + state = 73; + } + function step73() public { + require(state == 73); + state = 74; + } + function step74() public { + require(state == 74); + state = 75; + } + function step75() public { + require(state == 75); + state = 76; + } + function step76() public { + require(state == 76); + state = 77; + } + function step77() public { + require(state == 77); + state = 78; + } + function step78() public { + require(state == 78); + state = 79; + } + function step79() public { + require(state == 79); + state = 80; + } + function step80() public { + require(state == 80); + state = 81; + } + function step81() public { + require(state == 81); + state = 82; + } + function step82() public { + require(state == 82); + state = 83; + } + function step83() public { + require(state == 83); + state = 84; + } + function step84() public { + require(state == 84); + state = 85; + } + function step85() public { + require(state == 85); + state = 86; + } + function step86() public { + require(state == 86); + state = 87; + } + function step87() public { + require(state == 87); + state = 88; + } + function step88() public { + require(state == 88); + state = 89; + } + function step89() public { + require(state == 89); + state = 90; + } + function step90() public { + require(state == 90); + state = 91; + } + function step91() public { + require(state == 91); + state = 92; + } + function step92() public { + require(state == 92); + state = 93; + } + function step93() public { + require(state == 93); + state = 94; + } + function step94() public { + require(state == 94); + state = 95; + } + function step95() public { + require(state == 95); + state = 96; + } + function step96() public { + require(state == 96); + state = 97; + } + function step97() public { + require(state == 97); + state = 98; + } + function step98() public { + require(state == 98); + state = 99; + } + function step99() public { + require(state == 99); + state = 100; + } +} diff --git a/tests/solidity/basic/alex-sequence.yaml b/tests/solidity/basic/alex-sequence.yaml new file mode 100644 index 000000000..32d4d5fc9 --- /dev/null +++ b/tests/solidity/basic/alex-sequence.yaml @@ -0,0 +1,2 @@ +testMode: optimization +coverageFormats: ["txt","html","lcov"] \ No newline at end of file From 87e7b456d3f4946bc3813a8a1abb0d9b8294b898 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sun, 14 Dec 2025 17:01:57 +0100 Subject: [PATCH 2/7] fix: size --- .github/workflows/release-fork.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index 8d5e624b3..b9d95aefe 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: include: - - os: ubuntu-latest + - os: ubuntu-latest-m name: Linux (x86_64) tuple: x86_64-linux system: x86_64-linux @@ -28,7 +28,7 @@ jobs: tuple: aarch64-macos system: aarch64-darwin # Uncomment for additional platforms: - # - os: ubuntu-24.04-arm + # - os: ubuntu-latest-m # name: Linux (aarch64) # tuple: aarch64-linux # system: aarch64-linux From 6f528c198917f74c04c5102e8dd80ef225d7e36e Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sun, 14 Dec 2025 17:03:39 +0100 Subject: [PATCH 3/7] hope --- .github/workflows/release-fork.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index b9d95aefe..73221c5fa 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -28,7 +28,7 @@ jobs: tuple: aarch64-macos system: aarch64-darwin # Uncomment for additional platforms: - # - os: ubuntu-latest-m + # - os: ubuntu-24.04-arm # ARM runner required for aarch64 # name: Linux (aarch64) # tuple: aarch64-linux # system: aarch64-linux @@ -38,6 +38,16 @@ jobs: # system: x86_64-darwin steps: + - name: Free disk space + if: runner.os == 'Linux' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + df -h + - name: Checkout uses: actions/checkout@v4 From e661d285b250144b577fae1ebe9addb4fda0e5d2 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sun, 14 Dec 2025 21:42:11 +0100 Subject: [PATCH 4/7] feat: POST reload_corpus --- lib/Echidna/Server.hs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/Echidna/Server.hs b/lib/Echidna/Server.hs index 9f81c8101..e5bab5f13 100644 --- a/lib/Echidna/Server.hs +++ b/lib/Echidna/Server.hs @@ -1,20 +1,24 @@ module Echidna.Server where import Control.Concurrent +import Control.DeepSeq (force) import Control.Monad (when, void) import Data.Aeson import Data.Binary.Builder (fromLazyByteString) import Data.IORef import Data.Map qualified as Map +import Data.Set qualified as Set import Data.Time (LocalTime) import Data.Word (Word16) import Network.HTTP.Types (status200, status404) import Network.Wai (Application, responseLBS, pathInfo, requestMethod) import Network.Wai.EventSource (ServerEvent(..), eventSourceAppIO) import Network.Wai.Handler.Warp (run) +import System.FilePath (()) import EVM.Dapp (DappInfo(..)) +import Echidna.Output.Corpus (loadTxs) import Echidna.Output.Source (saveLcovHook) import Echidna.Types.Campaign (CampaignConf(..)) import Echidna.Types.Config (Env(..), EConfig(..)) @@ -97,6 +101,25 @@ runSSEServer serverStopVar env port nworkers = do respond $ responseLBS status200 [("Content-Type", "application/json")] (encode $ object ["file" .= fn]) + -- Reload corpus from disk + ("POST", ["reload_corpus"]) -> do + case env.cfg.campaignConf.corpusDir of + Nothing -> + respond $ responseLBS status404 [("Content-Type", "application/json")] + "{\"error\":\"No corpus directory configured\"}" + Just dir -> do + -- Load transactions from reproducers and coverage directories + ctxs1 <- loadTxs (dir "reproducers") + ctxs2 <- loadTxs (dir "coverage") + let allTxs = ctxs1 ++ ctxs2 + -- Add to corpus (with index based on current corpus size) + loaded <- atomicModifyIORef' env.corpusRef $ \corpus -> + let newEntries = Set.fromList $ zip [Set.size corpus + 1..] (map snd allTxs) + !corpus' = force $ Set.union corpus newEntries + in (corpus', length allTxs) + respond $ responseLBS status200 [("Content-Type", "application/json")] + (encode $ object ["loaded" .= loaded, "message" .= ("Loaded " ++ show loaded ++ " transaction sequences" :: String)]) + -- Unknown endpoint _ -> respond $ responseLBS status404 [("Content-Type", "application/json")] "{\"error\":\"Not found\"}" From 175b4ea59be4e04c06e5db565e9535127cd495e8 Mon Sep 17 00:00:00 2001 From: gustavo-grieco Date: Mon, 15 Dec 2025 12:17:31 +0100 Subject: [PATCH 5/7] avoid SSE to get stuck at the end of the execution --- lib/Echidna/Server.hs | 71 ++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/lib/Echidna/Server.hs b/lib/Echidna/Server.hs index 8524f472f..60cf57d73 100644 --- a/lib/Echidna/Server.hs +++ b/lib/Echidna/Server.hs @@ -1,7 +1,7 @@ module Echidna.Server where import Control.Concurrent -import Control.Monad (when, void) +import Control.Monad (forever, when, void) import Data.Aeson import Data.Binary.Builder (fromLazyByteString) import Data.IORef @@ -9,6 +9,7 @@ import Data.Time (LocalTime) import Data.Word (Word16) import Network.Wai.EventSource (ServerEvent(..), eventSourceAppIO) import Network.Wai.Handler.Warp (run) +import UnliftIO.STM (atomically, newBroadcastTChanIO, writeTChan, dupTChan, readTChan) import Echidna.Types.Config (Env(..)) import Echidna.Types.Worker @@ -40,36 +41,44 @@ runSSEServer :: MVar () -> Env -> Word16 -> Int -> IO () runSSEServer serverStopVar env port nworkers = do aliveRef <- newIORef nworkers sseChan <- dupChan env.eventQueue + broadcastChan <- newBroadcastTChanIO - let sseListener = do - aliveNow <- readIORef aliveRef - if aliveNow == 0 then - pure CloseEvent - else do - event@(_, campaignEvent) <- readChan sseChan - let eventName = \case - WorkerEvent _ _ workerEvent -> - case workerEvent of - TestFalsified _ -> "test_falsified" - TestOptimized _ -> "test_optimized" - NewCoverage {} -> "new_coverage" - SymExecLog _ -> "sym_exec_log" - SymExecError _ -> "sym_exec_error" - TxSequenceReplayed {} -> "tx_sequence_replayed" - TxSequenceReplayFailed {} -> "tx_sequence_replay_failed" - WorkerStopped _ -> "worker_stopped" - Failure _err -> "failure" - ReproducerSaved _ -> "saved_reproducer" - case campaignEvent of - WorkerEvent _ _ (WorkerStopped _) -> do - aliveAfter <- atomicModifyIORef' aliveRef (\n -> (n-1, n-1)) - when (aliveAfter == 0) $ putMVar serverStopVar () - _ -> pure () - pure $ ServerEvent - { eventName = Just (eventName campaignEvent) - , eventId = Nothing - , eventData = [ fromLazyByteString $ encode (SSE event) ] - } + void . forkIO . forever $ do + event@(_, campaignEvent) <- readChan sseChan + let eventName = \case + WorkerEvent _ _ workerEvent -> + case workerEvent of + TestFalsified _ -> "test_falsified" + TestOptimized _ -> "test_optimized" + NewCoverage {} -> "new_coverage" + SymExecLog _ -> "sym_exec_log" + SymExecError _ -> "sym_exec_error" + TxSequenceReplayed {} -> "tx_sequence_replayed" + TxSequenceReplayFailed {} -> "tx_sequence_replay_failed" + WorkerStopped _ -> "worker_stopped" + Failure _err -> "failure" + ReproducerSaved _ -> "saved_reproducer" + + let serverEvent = ServerEvent + { eventName = Just (eventName campaignEvent) + , eventId = Nothing + , eventData = [ fromLazyByteString $ encode (SSE event) ] + } + + atomically $ writeTChan broadcastChan serverEvent + + case campaignEvent of + WorkerEvent _ _ (WorkerStopped _) -> do + aliveAfter <- atomicModifyIORef' aliveRef (\n -> (n-1, n-1)) + when (aliveAfter == 0) $ do + atomically $ writeTChan broadcastChan CloseEvent + putMVar serverStopVar () + _ -> pure () + + let sseApp _req respond = do + myChan <- atomically $ dupTChan broadcastChan + let src = atomically $ readTChan myChan + eventSourceAppIO src _req respond void . forkIO $ do - run (fromIntegral port) $ eventSourceAppIO sseListener + run (fromIntegral port) sseApp From 5a00f7f4831c107f6fc934b5d3b96bcac9c7a51b Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 15 Dec 2025 15:59:21 +0100 Subject: [PATCH 6/7] chore: new release --- .github/workflows/release-fork.yml | 35 +++++++----------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index 73221c5fa..110c29b48 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -8,38 +8,19 @@ on: required: true default: 'dev' push: + branches: + - main tags: - "v*" jobs: build: - name: Build ${{ matrix.name }} + name: Build Linux (x86_64) timeout-minutes: 180 - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-latest-m - name: Linux (x86_64) - tuple: x86_64-linux - system: x86_64-linux - - os: macos-14 - name: macOS (aarch64) - tuple: aarch64-macos - system: aarch64-darwin - # Uncomment for additional platforms: - # - os: ubuntu-24.04-arm # ARM runner required for aarch64 - # name: Linux (aarch64) - # tuple: aarch64-linux - # system: aarch64-linux - # - os: macos-14 - # name: macOS (x86_64) - # tuple: x86_64-macos - # system: x86_64-darwin + runs-on: ubuntu-latest steps: - name: Free disk space - if: runner.os == 'Linux' run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/local/lib/android @@ -67,14 +48,14 @@ jobs: - name: Build redistributable echidna run: | - nix build ".#packages.${{ matrix.system }}.echidna-redistributable" --out-link redistributable - tar -czf "echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz" -C ./redistributable/bin/ echidna + nix build ".#packages.x86_64-linux.echidna-redistributable" --out-link redistributable + tar -czf "echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz" -C ./redistributable/bin/ echidna - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: echidna-${{ matrix.tuple }} - path: echidna-${{ steps.version.outputs.version }}-${{ matrix.tuple }}.tar.gz + name: echidna-x86_64-linux + path: echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz release: name: Create release From 240413110ba516d9f6f3aafe40394ba606b7b23a Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Mon, 15 Dec 2025 18:16:59 +0100 Subject: [PATCH 7/7] chore: release wtih cache --- .github/workflows/release-fork.yml | 50 ++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml index 110c29b48..81865b4b8 100644 --- a/.github/workflows/release-fork.yml +++ b/.github/workflows/release-fork.yml @@ -1,7 +1,7 @@ name: "Build & Release" on: - workflow_dispatch: # Manual trigger from GitHub UI + workflow_dispatch: inputs: version: description: 'Version name (e.g., 2.3.0-custom)' @@ -16,24 +16,23 @@ on: jobs: build: name: Build Linux (x86_64) - timeout-minutes: 180 + timeout-minutes: 30 runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} steps: - - name: Free disk space - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/lib/android - sudo rm -rf /opt/ghc - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo docker image prune --all --force - df -h - - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v14 + uses: DeterminateSystems/nix-installer-action@v21 + + - name: Configure Cachix + uses: cachix/cachix-action@v16 + with: + name: trailofbits + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - name: Set version id: version @@ -52,7 +51,7 @@ jobs: tar -czf "echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz" -C ./redistributable/bin/ echidna - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: echidna-x86_64-linux path: echidna-${{ steps.version.outputs.version }}-x86_64-linux.tar.gz @@ -60,13 +59,12 @@ jobs: release: name: Create release needs: [build] - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 + - name: Download artifacts + uses: actions/download-artifact@v6 with: pattern: echidna-* merge-multiple: true @@ -74,10 +72,24 @@ jobs: - name: List artifacts run: ls -la echidna-*.tar.gz - - name: Create GitHub release + - name: Create/Update rolling release (main branch) + if: github.ref == 'refs/heads/main' + uses: softprops/action-gh-release@v2 + with: + tag_name: latest + name: "Echidna Latest (main)" + body: | + Rolling release from main branch. + Version: ${{ needs.build.outputs.version }} + Commit: ${{ github.sha }} + prerelease: true + files: ./echidna-*.tar.gz + + - name: Create tagged release + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' uses: softprops/action-gh-release@v2 with: draft: true - name: "Echidna (custom build)" + name: "Echidna ${{ needs.build.outputs.version }}" tag_name: ${{ github.ref_name || format('v{0}', github.event.inputs.version) }} files: ./echidna-*.tar.gz