From fce498ce450f127d2891524c916ea605e6c8f9bf Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 26 May 2026 04:26:33 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat(events):=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20SHiP=20action=5Ftrace=20=D0=B2=20ActionEvent=20(?= =?UTF-8?q?transaction=5Fid,=20creator=5Faction=5Fordinal,=20receipt.auth?= =?UTF-8?q?=5Fsequence,=20=E2=80=A6)=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 25 +++++++++++++---- README.md | 22 +++++++++++++++ packages/parser2/package.json | 2 +- packages/parser2/src/core/BlockProcessor.ts | 6 ++++ packages/parser2/src/types.ts | 14 +++++++++- .../parser2/test/unit/blockProcessor.test.ts | 21 ++++++++++++-- packages/parser2/test/unit/eventId.test.ts | 6 ++++ packages/parser2/test/unit/filters.test.ts | 6 ++++ packages/ship-reader/package.json | 2 +- packages/ship-reader/src/ShipProtocol.ts | 28 +++++++++++++++++-- packages/ship-reader/src/index.ts | 2 ++ packages/ship-reader/src/types/ship.ts | 16 +++++++++++ 12 files changed, 137 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a10355..7d58910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,21 +85,36 @@ jobs: # передачу команды контейнеру. Монтируем config.ini (SHiP на 8080) и genesis.json. - name: Start blockchain node run: | + # Раннер self-hosted и персистентный: упавший прогон оставляет контейнер + # с фиксированным именем blockchain, и следующий docker run падает с + # exit 125 (Conflict, name already in use). Сносим остаток перед стартом. + docker rm -f blockchain 2>/dev/null || true + # nodeos на старте генерирует protocol_features/*.json В смонтированный + # config-каталог. Если монтировать сам workspace, эти файлы остаются в нём + # (untracked, root-owned — checkout --force их не чистит) и на следующем + # прогоне ломают узел: 'plugin_config_exception: Incorrect plugin + # configuration'. Монтируем pristine-копию во временном каталоге — узел + # пишет сгенерированное туда, workspace остаётся чистым. + CONFIG_DIR=$(mktemp -d) + cp "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config/"* "$CONFIG_DIR/" docker run -d --name blockchain \ -p 8888:8888 -p 8080:8080 -p 9876:9876 \ - -v "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config:/mnt/dev/config" \ + -v "$CONFIG_DIR:/mnt/dev/config" \ dicoop/blockchain:v5.1.0-dev \ /bin/bash -c '/usr/local/bin/nodeos -d /mnt/dev/data -p eosio --config-dir /mnt/dev/config --genesis-json /mnt/dev/config/genesis.json --disable-replay-opts' sleep 3 - name: Wait for Chain API run: | - for i in $(seq 1 60); do - if curl -sf http://localhost:8888/v1/chain/get_info > /dev/null 2>&1; then + # Бюджет 120 попыток × (≤2с curl + 2с sleep) ≈ до 8 мин: на нагруженном + # раннере nodeos поднимается медленно (наблюдали >6 мин до get_info), а + # curl без --max-time подвисал на ещё-не-отвечающем порту и съедал бюджет. + for i in $(seq 1 120); do + if curl -sf --max-time 2 http://localhost:8888/v1/chain/get_info > /dev/null 2>&1; then echo "Chain API ready after $i attempts" exit 0 fi - echo " attempt $i/60..." - sleep 2 + echo " attempt $i/120..." + sleep 4 done echo "ERROR: Chain API did not become ready. Container logs:" docker logs blockchain | tail -100 diff --git a/README.md b/README.md index bf2f466..d54bcb6 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,28 @@ docker run --rm --network host \ - [Redis key taxonomy](docs/redis-key-taxonomy.md) - [Disaster recovery / fork scenarios](docs/disaster-recovery.md) +## Известные ограничения + +### Схлопывание дельт create+remove внутри одного блока + +Парсер **не увидит** жизненный цикл строки таблицы, у которой `emplace` и `erase` произошли **внутри одного блока** (даже в разных транзакциях). Это поведение унаследовано от upstream-плагина `state_history_plugin` (SHiP) ноджеса: на уровне `chainbase::undo_index::on_remove` строка, созданная в текущей undo session и тут же удалённая, физически уничтожается без записи в `_removed_values` — это by-design оптимизация для отката блока (нечего откатывать), но она же делает event невидимым для SHiP-снимка состояния. SHiP пакует только то, что есть в undo session, и не имеет шанса увидеть транзиентную пару. + +**Симптомы:** +- На цепочке таблица пустая (получена и тут же удалена). +- ACTION-логи и `getclearance`/`apprvappndx`-подобные действия видны нормально — экшены идут параллельным потоком и не зависят от undo. +- В Redis stream / БД-индексе нет ни `present:true`, ни `present:false` события для конкретного `primary_key`. + +**Когда это всплывает на практике:** +- Контракт делает `emplace + erase` одной сущности в одной транзакции (anti-pattern, но встречается). +- Несколько коротких транзакций над одной сущностью пакуются производителем блоков в один блок (массовый импорт, auto-approve, bulk-операции, миграции, batch-сценарии). + +**Workaround на стороне приложения:** +- Разнести `emplace` и `erase` по разным блокам — в seed/bulk-сценариях добавить sleep ≥ 1 × `block_time` (≈500 мс для EOSIO) между транзакциями. В production-flow ручного approve'а это не нужно — между нажатием кнопки и второй транзакцией всегда проходит более одного блока. +- Если транзиентные пары — часть нормального дизайна контракта, рассмотреть переписывание на `modify(status="approved")` вместо `erase`: SHiP корректно эмиттит дельту изменения статуса. + +**Системный фикс (планируется):** +Drop-in патч `chainbase` + `state_history_plugin` в нашем форке `~/coopos` (uENOSIO) — отдельный список `_transient_removed_values` в undo_index + паковка пар `[present:true, present:false]` в `table_delta_v0` без изменения wire-формата. Wire-совместимо со всеми существующими SHiP-клиентами; после деплоя форка парсер автоматически начнёт видеть транзиентные пары без изменений в коде. Технические детали и план работ — в задаче `f3-shipchainbase-patch-vidimost-tranzientnykh-delt-createremove-vnutri-bloka.md` проекта parser2 в `_blago`. + ## Разработка ### Структура монорепы diff --git a/packages/parser2/package.json b/packages/parser2/package.json index 452610d..5bbc8ea 100644 --- a/packages/parser2/package.json +++ b/packages/parser2/package.json @@ -1,6 +1,6 @@ { "name": "@coopenomics/parser2", - "version": "1.0.3", + "version": "1.1.0", "description": "Universal EOSIO/Antelope SHiP-to-Redis blockchain indexer (parser)", "license": "MIT", "author": "Coopenomics contributors", diff --git a/packages/parser2/src/core/BlockProcessor.ts b/packages/parser2/src/core/BlockProcessor.ts index 96a9be4..569b696 100644 --- a/packages/parser2/src/core/BlockProcessor.ts +++ b/packages/parser2/src/core/BlockProcessor.ts @@ -136,13 +136,19 @@ export class BlockProcessor { block_num: blockNum, block_time: blockTime, block_id: blockId, + transaction_id: trace.transactionId, account: trace.account, name: trace.name, authorization: [...trace.authorization], data, action_ordinal: trace.actionOrdinal, + creator_action_ordinal: trace.creatorActionOrdinal, global_sequence: trace.globalSequence, receipt: trace.receipt, + context_free: trace.contextFree, + elapsed: trace.elapsed, + console: trace.console, + account_ram_deltas: [...trace.accountRamDeltas], } actionEvents.push({ ...partial, event_id: computeEventId(partial) }) diff --git a/packages/parser2/src/types.ts b/packages/parser2/src/types.ts index 35168ec..52fdf31 100644 --- a/packages/parser2/src/types.ts +++ b/packages/parser2/src/types.ts @@ -8,7 +8,7 @@ * повторные доставки. */ -import type { ActionAuthorization, ActionReceipt } from '@coopenomics/coopos-ship-reader' +import type { ActionAuthorization, ActionReceipt, AccountRamDelta } from '@coopenomics/coopos-ship-reader' /** Событие вызова смарт-контракта (inline action). */ export interface ActionEvent { @@ -20,6 +20,8 @@ export interface ActionEvent { /** ISO-8601 время блока из трассировки транзакции. */ block_time: string block_id: string + /** Хеш транзакции, в которой исполнено действие. */ + transaction_id: string /** Аккаунт-владелец контракта (account). */ account: string /** Имя действия (action name). */ @@ -29,10 +31,20 @@ export interface ActionEvent { data: Record /** Порядковый номер действия внутри транзакции (1-based). */ action_ordinal: number + /** Порядковый номер родительского (создавшего) действия; 0 — если действие верхнего уровня. */ + creator_action_ordinal: number /** Глобальная уникальная последовательность — монотонный счётчик действий в цепи. */ global_sequence: bigint /** Квитанция об исполнении; null если нет трассировки. */ receipt: ActionReceipt | null + /** Является ли действие context-free. */ + context_free: boolean + /** Время исполнения действия в микросекундах. */ + elapsed: number + /** Консольный вывод действия (eosio::print и т.п.). */ + console: string + /** Изменения RAM по аккаунтам в результате действия. */ + account_ram_deltas: AccountRamDelta[] } /** Изменение строки в пользовательской таблице смарт-контракта (contract_row delta). */ diff --git a/packages/parser2/test/unit/blockProcessor.test.ts b/packages/parser2/test/unit/blockProcessor.test.ts index 5114dfe..6f98c38 100644 --- a/packages/parser2/test/unit/blockProcessor.test.ts +++ b/packages/parser2/test/unit/blockProcessor.test.ts @@ -57,8 +57,13 @@ function makeBlock(blockNum = 1, numTraces = 0, numDeltas = 0): ShipBlock { authorization: [{ actor: 'alice', permission: 'active' }], actRaw: new Uint8Array([1, 2, 3]), actionOrdinal: i + 1, + creatorActionOrdinal: 0, globalSequence: BigInt(100 + i), receipt: null, + contextFree: false, + elapsed: 0, + console: '', + accountRamDeltas: [], blockNum, blockId: 'a'.repeat(64), blockTime: '2024-01-01T00:00:00.000', @@ -176,8 +181,13 @@ describe('BlockProcessor — ABI updates (Story 4.3)', () => { authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, + creatorActionOrdinal: 0, globalSequence: BigInt(1), receipt: null, + contextFree: false, + elapsed: 0, + console: '', + accountRamDeltas: [], blockNum: 500, blockId: 'c'.repeat(64), blockTime: '2024-01-01T00:00:00.000', @@ -210,8 +220,13 @@ describe('BlockProcessor — ABI updates (Story 4.3)', () => { authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, + creatorActionOrdinal: 0, globalSequence: BigInt(2), receipt: null, + contextFree: false, + elapsed: 0, + console: '', + accountRamDeltas: [], blockNum: 501, blockId: 'c'.repeat(64), blockTime: '2024-01-01T00:00:00.000', @@ -324,8 +339,8 @@ describe('BlockProcessor — native deltas (Epic 6)', () => { lastIrreversible: { blockNum: 20, blockId: 'b'.repeat(64) }, prevBlock: null, traces: [ - { account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, globalSequence: BigInt(1), receipt: null, blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) }, - { account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 2, globalSequence: BigInt(2), receipt: null, blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) }, + { account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, creatorActionOrdinal: 0, globalSequence: BigInt(1), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) }, + { account: 'eosio.token', name: 'transfer', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 2, creatorActionOrdinal: 0, globalSequence: BigInt(2), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 20, blockId: 'b'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'c'.repeat(64) }, ], deltas: [ { name: 'contract_row', present: true, rowRaw: new Uint8Array([1]), code: 'eosio.token', scope: 'alice', table: 'accounts', primaryKey: '1' }, @@ -355,7 +370,7 @@ describe('BlockProcessor — native deltas (Epic 6)', () => { head: { blockNum: 5, blockId: 'd'.repeat(64) }, lastIrreversible: { blockNum: 5, blockId: 'd'.repeat(64) }, prevBlock: null, - traces: [{ account: 'eosio', name: 'newaccount', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, globalSequence: BigInt(10), receipt: null, blockNum: 5, blockId: 'd'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'e'.repeat(64) }], + traces: [{ account: 'eosio', name: 'newaccount', authorization: [], actRaw: new Uint8Array([1]), actionOrdinal: 1, creatorActionOrdinal: 0, globalSequence: BigInt(10), receipt: null, contextFree: false, elapsed: 0, console: '', accountRamDeltas: [], blockNum: 5, blockId: 'd'.repeat(64), blockTime: '2024-01-01T00:00:00.000', transactionId: 'e'.repeat(64) }], deltas: [ { name: 'contract_row', present: true, rowRaw: new Uint8Array([1]), code: 'eosio', scope: 'eosio', table: 'global', primaryKey: '0' }, { name: 'permission' as never, present: true, rowRaw: new Uint8Array([1]) }, diff --git a/packages/parser2/test/unit/eventId.test.ts b/packages/parser2/test/unit/eventId.test.ts index 0744038..925ebe0 100644 --- a/packages/parser2/test/unit/eventId.test.ts +++ b/packages/parser2/test/unit/eventId.test.ts @@ -15,13 +15,19 @@ function makeAction(override: Partial } interface RawActionReceipt { @@ -69,6 +74,9 @@ interface RawActionReceipt { act_digest: string global_sequence: string recv_sequence: string + // auth_sequence — массив пар (account, sequence). wharfkit может отдать как + // объект {account, sequence} или как кортеж [account, sequence]. + auth_sequence?: Array<{ account: string; sequence: string | number } | [string, string | number]> code_sequence: number abi_sequence: number } @@ -142,17 +150,28 @@ export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number, const receiptData = Array.isArray(at.receipt) ? (at.receipt[1] as RawActionTrace['receipt']) : at.receipt + const authSequence: AuthSequence[] = (receiptData?.auth_sequence ?? []).map(pair => + Array.isArray(pair) + ? { account: String(pair[0]), sequence: String(pair[1]) } + : { account: String(pair.account), sequence: String(pair.sequence) }, + ) const receipt: ActionReceipt | null = receiptData ? ({ receiver: String(receiptData.receiver), actDigest: String(receiptData.act_digest), globalSequence: BigInt(receiptData.global_sequence), recvSequence: BigInt(receiptData.recv_sequence), + authSequence, codeSequence: receiptData.code_sequence, abiSequence: receiptData.abi_sequence, } as ActionReceipt) : null + const accountRamDeltas: AccountRamDelta[] = (at.account_ram_deltas ?? []).map(d => ({ + account: String(d.account), + delta: Number(d.delta), + })) + // action_trace_v1 не имеет top-level global_sequence (только в receipt). // Fallback: если at.global_sequence undefined — берём из receipt. const topGlobalSeq = at.global_sequence ?? receiptData?.global_sequence @@ -162,8 +181,13 @@ export function decodeBlocksResult(raw: unknown, abi: ShipAbi, blockNum: number, authorization, actRaw: at.act.data instanceof Bytes ? at.act.data.array : Uint8Array.from([]), actionOrdinal: at.action_ordinal, + creatorActionOrdinal: at.creator_action_ordinal ?? 0, globalSequence: topGlobalSeq !== undefined ? BigInt(topGlobalSeq) : 0n, receipt, + contextFree: at.context_free ?? false, + elapsed: at.elapsed !== undefined ? Number(at.elapsed) : 0, + console: at.console ?? '', + accountRamDeltas, blockNum: thisBlock.blockNum, blockId: thisBlock.blockId, blockTime, diff --git a/packages/ship-reader/src/index.ts b/packages/ship-reader/src/index.ts index b1b8ec4..cf207a0 100644 --- a/packages/ship-reader/src/index.ts +++ b/packages/ship-reader/src/index.ts @@ -23,6 +23,8 @@ export type { BlockPosition, ActionReceipt, ActionAuthorization, + AuthSequence, + AccountRamDelta, } from './types/ship.js' export type { diff --git a/packages/ship-reader/src/types/ship.ts b/packages/ship-reader/src/types/ship.ts index ed08be2..b7371d1 100644 --- a/packages/ship-reader/src/types/ship.ts +++ b/packages/ship-reader/src/types/ship.ts @@ -8,11 +8,22 @@ export interface ActionAuthorization { readonly permission: string } +export interface AuthSequence { + readonly account: string + readonly sequence: string +} + +export interface AccountRamDelta { + readonly account: string + readonly delta: number +} + export interface ActionReceipt { readonly receiver: string readonly actDigest: string readonly globalSequence: bigint readonly recvSequence: bigint + readonly authSequence: readonly AuthSequence[] readonly codeSequence: number readonly abiSequence: number } @@ -23,8 +34,13 @@ export interface ShipTrace { readonly authorization: readonly ActionAuthorization[] readonly actRaw: Uint8Array readonly actionOrdinal: number + readonly creatorActionOrdinal: number readonly globalSequence: bigint readonly receipt: ActionReceipt | null + readonly contextFree: boolean + readonly elapsed: number + readonly console: string + readonly accountRamDeltas: readonly AccountRamDelta[] readonly blockNum: number readonly blockId: string readonly blockTime: string -- 2.52.0 From 9f21c61a759d86b8cecfcc578f2a4ed8efc5a249 Mon Sep 17 00:00:00 2001 From: coopops Date: Tue, 26 May 2026 11:56:12 +0000 Subject: [PATCH 2/5] =?UTF-8?q?ci:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D0=BA=20=D0=BF=D1=80=D0=BE=D0=B3=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B0=20dev=20=E2=80=94=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D0=BA=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B8=20=D0=B4=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D1=80=D0=B0=D0=BD=D0=BD=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прогон 182 упал на стадии cache-restore/install (инфра, не код): typecheck локально зелёный, паритет пакетов с dev подтверждён. Co-Authored-By: Claude Opus 4.7 -- 2.52.0 From d575da529bae9316c2461c4a850eee8defd6335e Mon Sep 17 00:00:00 2001 From: coopops Date: Tue, 26 May 2026 12:52:02 +0000 Subject: [PATCH 3/5] =?UTF-8?q?ci:=20=D0=BF=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=BD=D0=BE=D0=B4=D1=83=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20docker=20cp=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20bind-mount=20(DinD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раннер — docker-in-docker (docker.sock хоста в job-контейнере), поэтому пути bind-mount (-v) резолвятся на ФС хоста: ни mktemp, ни workspace демону не видны → монтировался пустой каталог, nodeos падал с 'genesis file does not exist'. Конфиг теперь кладём в контейнер через docker cp (tar по API, от путей хоста не зависит), config-dir=/mnt/cfg (docker cp не создаёт родителей, а /mnt/dev/config в образе нет). Проверено локально: chain API get_info + SHiP 101 Switching Protocols. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d58910..7ecd67d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,22 +86,26 @@ jobs: - name: Start blockchain node run: | # Раннер self-hosted и персистентный: упавший прогон оставляет контейнер - # с фиксированным именем blockchain, и следующий docker run падает с - # exit 125 (Conflict, name already in use). Сносим остаток перед стартом. + # с фиксированным именем blockchain, и следующий старт падает с exit 125 + # (Conflict, name already in use). Сносим остаток перед стартом. docker rm -f blockchain 2>/dev/null || true - # nodeos на старте генерирует protocol_features/*.json В смонтированный - # config-каталог. Если монтировать сам workspace, эти файлы остаются в нём - # (untracked, root-owned — checkout --force их не чистит) и на следующем - # прогоне ломают узел: 'plugin_config_exception: Incorrect plugin - # configuration'. Монтируем pristine-копию во временном каталоге — узел - # пишет сгенерированное туда, workspace остаётся чистым. - CONFIG_DIR=$(mktemp -d) - cp "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config/"* "$CONFIG_DIR/" - docker run -d --name blockchain \ + # Конфиг кладём в контейнер через docker cp, а НЕ через bind-mount (-v). + # Раннер работает по схеме docker-in-docker (docker.sock хоста проброшен + # в job-контейнер), поэтому пути bind-mount резолвятся на ФС ХОСТА, а не + # внутри job-контейнера: ни mktemp, ни workspace хостовому демону не видны + # → монтируется ПУСТОЙ каталог (nodeos пишет туда protocol_features, но + # genesis.json/config.ini отсутствуют → 'genesis file does not exist'). + # docker cp передаёт файлы tar-стримом по API и от путей хоста не зависит. + # /mnt/dev/config в образе нет (его создавал сам mount), а docker cp не + # создаёт родительские каталоги — поэтому config-dir = /mnt/cfg (родитель + # /mnt существует). protocol_features узел пишет туда же, в свежесозданный + # контейнер — никакого мусора между прогонами. + docker create --name blockchain \ -p 8888:8888 -p 8080:8080 -p 9876:9876 \ - -v "$CONFIG_DIR:/mnt/dev/config" \ dicoop/blockchain:v5.1.0-dev \ - /bin/bash -c '/usr/local/bin/nodeos -d /mnt/dev/data -p eosio --config-dir /mnt/dev/config --genesis-json /mnt/dev/config/genesis.json --disable-replay-opts' + /bin/bash -c 'mkdir -p /mnt/data && exec /usr/local/bin/nodeos -d /mnt/data -p eosio --config-dir /mnt/cfg --genesis-json /mnt/cfg/genesis.json --disable-replay-opts' + docker cp "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config" blockchain:/mnt/cfg + docker start blockchain sleep 3 - name: Wait for Chain API run: | -- 2.52.0 From 37efb4834e16022d6d870c5e140387990c154fc5 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 26 May 2026 15:01:55 +0000 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BD=D0=B0=20DinD-=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D0=B5=D1=80=D0=B5=20(docker=20cp=20+=20=D1=81?= =?UTF-8?q?=D0=B5=D1=82=D1=8C=20job=D0=B0=20+=20redis=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8)=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Конфиг ноды через docker cp, нода в сети job-контейнера по IP, Redis по имени сервиса. Полный конвейер зелёный (integration 1 passed против живой ноды + SHiP + Redis). Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ecd67d..ce8740d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,11 +101,23 @@ jobs: # /mnt существует). protocol_features узел пишет туда же, в свежесозданный # контейнер — никакого мусора между прогонами. docker create --name blockchain \ - -p 8888:8888 -p 8080:8080 -p 9876:9876 \ dicoop/blockchain:v5.1.0-dev \ /bin/bash -c 'mkdir -p /mnt/data && exec /usr/local/bin/nodeos -d /mnt/data -p eosio --config-dir /mnt/cfg --genesis-json /mnt/cfg/genesis.json --disable-replay-opts' docker cp "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config" blockchain:/mnt/cfg + # Сеть в DinD: порт-маппинг -p публикует на ХОСТЕ, а шаги job'а крутятся + # внутри job-контейнера — его localhost хостовых портов не видит (поэтому + # localhost:8888 был недостижим). redis-сервис доступен по localhost лишь + # потому, что его сеть настраивает сам Gitea. Свою ноду подключаем в ту же + # сеть, что и job-контейнер, и адресуем по её IP на этой сети (надёжнее + # DNS — работает и на bridge); IP пробрасываем в env последующих шагов. + SELF=$(cat /etc/hostname) + NET=$(docker inspect "$SELF" --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' | grep -v '^bridge$' | head -1) + [ -z "$NET" ] && NET=bridge + docker network connect "$NET" blockchain docker start blockchain + NODE_HOST=$(docker inspect blockchain --format "{{(index .NetworkSettings.Networks \"$NET\").IPAddress}}") + echo "Нода в сети '$NET' с IP $NODE_HOST" + echo "NODE_HOST=$NODE_HOST" >> "$GITHUB_ENV" sleep 3 - name: Wait for Chain API run: | @@ -113,7 +125,7 @@ jobs: # раннере nodeos поднимается медленно (наблюдали >6 мин до get_info), а # curl без --max-time подвисал на ещё-не-отвечающем порту и съедал бюджет. for i in $(seq 1 120); do - if curl -sf --max-time 2 http://localhost:8888/v1/chain/get_info > /dev/null 2>&1; then + if curl -sf --max-time 2 "http://${NODE_HOST}:8888/v1/chain/get_info" > /dev/null 2>&1; then echo "Chain API ready after $i attempts" exit 0 fi @@ -125,9 +137,12 @@ jobs: exit 1 - run: pnpm --filter @coopenomics/parser2 test:integration env: - REDIS_URL: redis://localhost:6379 - CHAIN_URL: http://localhost:8888 - SHIP_URL: ws://localhost:8080 + # В Gitea/act job крутится в контейнере, а services: — контейнеры-соседи + # в той же сети, доступные ПО ИМЕНИ СЕРВИСА (redis), а не через localhost + # (localhost сработал бы на GitHub-хостед раннере, но не в DinD). + REDIS_URL: redis://redis:6379 + CHAIN_URL: http://${{ env.NODE_HOST }}:8888 + SHIP_URL: ws://${{ env.NODE_HOST }}:8080 - name: Show blockchain logs on failure if: failure() run: docker logs blockchain | tail -200 -- 2.52.0 From 05d413b37f46b9ddf7fc1a32aa1f3a7850821f6d Mon Sep 17 00:00:00 2001 From: coopops Date: Tue, 26 May 2026 16:17:12 +0000 Subject: [PATCH 5/5] =?UTF-8?q?ci:=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B0=20pull=5Frequest,=20=D1=83=D0=B1=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D1=82=D1=80=D0=B8=D0=B3=D0=B3=D0=B5=D1=80=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прогон на push в dev/main дублировал проверку, уже сделанную на PR, и жёг по ~47 мин на нагруженном раннере. Оставляем только pull_request (main, dev): ветки в эти ветки попадают через PR, который и валидируется; релиз main триггерит отдельный release.yml. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce8740d..28a3d93 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,6 @@ name: CI on: - push: - branches: [main, dev] pull_request: branches: [main, dev] -- 2.52.0