From b70b609c0413a18b2af1c9318beb45d7047b678c Mon Sep 17 00:00:00 2001 From: HusseinAdeiza Date: Wed, 14 Oct 2026 11:22:59 +0200 Subject: [PATCH 1/2] fix(consensus): guard against zero total stake in VRF validator selection get_validators_for_transaction divided each validator's stake by the total stake without checking for zero. When all validators have stake=0 (e.g. internally-created validators in protocol_rpc/endpoints.py), this raised ZeroDivisionError and crashed the consensus worker, halting transaction processing. Fall back to a uniform probability distribution when the total stake is zero; non-zero totals are unaffected. Adds a unit test covering the zero-stake path. Closes #1732 Signed-off-by: HusseinAdeiza --- backend/consensus/vrf.py | 8 +++++++- tests/unit/test_vrf.py | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/consensus/vrf.py b/backend/consensus/vrf.py index 55c953600..c13daafcb 100644 --- a/backend/consensus/vrf.py +++ b/backend/consensus/vrf.py @@ -18,7 +18,13 @@ def get_validators_for_transaction( num_validators = min(num_validators, len(nodes)) total_stake = sum(validator["stake"] for validator in nodes) - probabilities = [validator["stake"] / total_stake for validator in nodes] + if total_stake == 0: + # All validators have zero stake (e.g. internally-created validators in + # protocol_rpc/endpoints.py). Dividing by zero crashed the consensus + # worker, so fall back to a uniform distribution across equal-stake nodes. + probabilities = [1 / len(nodes)] * len(nodes) + else: + probabilities = [validator["stake"] / total_stake for validator in nodes] selected_validators = rng.choice( nodes, diff --git a/tests/unit/test_vrf.py b/tests/unit/test_vrf.py index 95473770e..9c472438e 100644 --- a/tests/unit/test_vrf.py +++ b/tests/unit/test_vrf.py @@ -1,6 +1,8 @@ from backend.consensus.vrf import get_validators_for_transaction from unittest.mock import Mock +import pytest + def list_of_dicts_to_set(list_of_dicts: list[dict]) -> set: return set(map(lambda x: tuple(x.items()), list_of_dicts)) @@ -66,3 +68,27 @@ def choice_mock(a, p, size, replace): rng.choice.assert_called_once() assert validators == [{"stake": 3}, {"stake": 2}, {"stake": 1}] + + +def test_get_validators_for_transaction_all_zero_stake(): + """ + Tests that a uniform probability is used when the total stake is zero. + + Previously this divided by zero and crashed the consensus worker + (ZeroDivisionError). Internally-created validators with stake=0 make this a + reachable path, so equal-stake nodes must fall back to a uniform distribution. + """ + nodes = [{"stake": 0}, {"stake": 0}, {"stake": 0}] + + def choice_mock(a, p, size, replace): + assert p == [1 / 3, 1 / 3, 1 / 3] + assert sum(p) == pytest.approx(1.0) + return list(a) + + rng = Mock() + rng.choice.side_effect = choice_mock + + validators = get_validators_for_transaction(nodes, 3, rng=rng) + + rng.choice.assert_called_once() + assert validators == nodes From 4cdd55a6340b7b0753498b2dc4dba61a313ac993 Mon Sep 17 00:00:00 2001 From: Hussein Adeiza Date: Wed, 14 Oct 2026 11:23:29 +0200 Subject: [PATCH 2/2] chore: drop inherited workflow files (PAT lacks workflow scope) --- .github/workflows/.pre-commit.yml | 30 - .github/workflows/README.md | 15 - .../backend_integration_tests_pr.yml | 551 ------------ .github/workflows/branch-policy.yml | 24 - .github/workflows/conventional-commit-pr.yml | 16 - .../workflows/docker-build-and-push-all.yml | 102 --- .../workflows/docker-build-and-push-image.yml | 121 --- .../docker-build-test-native-arm.yml | 104 --- .github/workflows/e2e-housekeeper.yml | 84 -- .github/workflows/e2e.yml | 798 ------------------ .github/workflows/fast-forward-main.yaml | 57 -- .github/workflows/frontend-unit-tests.yml | 36 - .github/workflows/genvm-lint.yml | 66 -- .github/workflows/load-test-oha.yml | 603 ------------- .github/workflows/manual-docker-release.yml | 78 -- .github/workflows/release-from-tag.yml | 173 ---- .github/workflows/retarget-main-prs.yaml | 53 -- .github/workflows/scan-codeql.yml | 52 -- .github/workflows/test-semgrep.yml | 35 - .github/workflows/unit-tests-pr.yml | 76 -- 20 files changed, 3074 deletions(-) delete mode 100644 .github/workflows/.pre-commit.yml delete mode 100644 .github/workflows/README.md delete mode 100644 .github/workflows/backend_integration_tests_pr.yml delete mode 100644 .github/workflows/branch-policy.yml delete mode 100644 .github/workflows/conventional-commit-pr.yml delete mode 100644 .github/workflows/docker-build-and-push-all.yml delete mode 100644 .github/workflows/docker-build-and-push-image.yml delete mode 100644 .github/workflows/docker-build-test-native-arm.yml delete mode 100644 .github/workflows/e2e-housekeeper.yml delete mode 100644 .github/workflows/e2e.yml delete mode 100644 .github/workflows/fast-forward-main.yaml delete mode 100644 .github/workflows/frontend-unit-tests.yml delete mode 100644 .github/workflows/genvm-lint.yml delete mode 100644 .github/workflows/load-test-oha.yml delete mode 100644 .github/workflows/manual-docker-release.yml delete mode 100644 .github/workflows/release-from-tag.yml delete mode 100644 .github/workflows/retarget-main-prs.yaml delete mode 100644 .github/workflows/scan-codeql.yml delete mode 100644 .github/workflows/test-semgrep.yml delete mode 100644 .github/workflows/unit-tests-pr.yml diff --git a/.github/workflows/.pre-commit.yml b/.github/workflows/.pre-commit.yml deleted file mode 100644 index 0f9766121..000000000 --- a/.github/workflows/.pre-commit.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Sometimes this workflow fails due to problems in the target PR branch. I (Agustín Díaz) don't know yet the reason why the code is analyzing for the merge commit instead of the PR branch. -# Anyways, it's an indicator that the target PR branch is not clean. This is a good thing because it helps to keep the codebase clean. -# To fix this, rebase the PR branch with the target branch and check `pre-commit run --all-files` locally. -name: pre-commit - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - # Set up Python - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: pip - - run: pip install black - # Set up Node.js - - uses: actions/setup-node@v6 - with: - node-version: 21 - cache: "npm" - cache-dependency-path: frontend/package-lock.json - - run: npm ci - working-directory: frontend - # Run pre-commit - - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index 85e0ebe71..000000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Studio CI Notes - -`branch-policy.yml` keeps the release-train model explicit: - -- independently releasable work may target a stable branch directly -- multi-feature or cross-repo train work goes to the active `*-dev` integration - branch -- promotion PRs into a release branch normally come from the matching `*-dev` - branch, for example `v0.123-dev` into `v0.123` -- `main` is treated as the static/default GitHub branch, not a release surface -- `master`, stale release branches, `release-from-main.yml`, and - `release.config.js` are treated as invalid release surfaces -- releases must go through version tags validated by `release-from-tag.yml` - -See `docs/BRANCHING.md` for the contributor-facing branch model. diff --git a/.github/workflows/backend_integration_tests_pr.yml b/.github/workflows/backend_integration_tests_pr.yml deleted file mode 100644 index 53309f95a..000000000 --- a/.github/workflows/backend_integration_tests_pr.yml +++ /dev/null @@ -1,551 +0,0 @@ -name: CI - -on: - pull_request: - types: - - opened - - labeled - - synchronize - pull_request_review: - types: - - submitted - - edited - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: ${{ github.event.action == 'synchronize' }} - -jobs: - triggers: - name: Get Triggers - runs-on: ubuntu-latest - outputs: - is_pull_request_opened: ${{ github.event_name == 'pull_request' && github.event.action == 'opened'}} - is_pull_request_review_approved: ${{ github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED'}} - is_pull_request_labeled_with_run_tests: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests')}} - steps: - - run: true - - test: - needs: triggers - if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }} - - runs-on: ubuntu-latest - - env: - PYTHONPATH: ${{ github.workspace }} - COMPOSE_PROFILES: hardhat - GENVM_CACHE_DIR: /tmp/genvm-cache - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Copy .env file - run: cp .env.example .env - - - name: Resolve GenVM binding - env: - GH_TOKEN: ${{ github.token }} - run: | - # Binding model: codebase default < env override. - # third_party/genvm/version (same convention as genlayer-node) pins - # the GenVM this branch's host protocol speaks; it is flipped to the - # release tag at release cut. The e2e pipeline / release train can - # override by injecting GENVM_TAG or GENVM_REF into the environment. - # Exact vX.Y.Z[-suffix] values use release assets. Source pins must be - # explicit : values so builds cannot drift with a branch. - if [[ -n "${GENVM_TAG:-}" && -n "${GENVM_REF:-}" ]]; then - echo "::error::GENVM_TAG and GENVM_REF are both set; they are mutually exclusive" - exit 1 - fi - GENVM_TAG_VALUE="${GENVM_TAG:-}" - GENVM_REF_VALUE="${GENVM_REF:-}" - GENVM_SOURCE_MODE_VALUE="${GENVM_SOURCE_MODE:-}" - if [[ -z "$GENVM_TAG_VALUE" && -z "$GENVM_REF_VALUE" ]]; then - if [[ ! -f third_party/genvm/version ]]; then - echo "::error::third_party/genvm/version is missing and no GENVM_TAG/GENVM_REF env override is set" - exit 1 - fi - BINDING="$(head -n1 third_party/genvm/version | sed -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - if [[ "$BINDING" =~ ^\".*\"$ || "$BINDING" =~ ^\'.*\'$ ]]; then - BINDING="${BINDING:1:${#BINDING}-2}" - BINDING="$(printf '%s' "$BINDING" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - fi - if [[ -z "$BINDING" ]]; then - echo "::error::third_party/genvm/version is empty" - exit 1 - fi - if [[ "$BINDING" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - GENVM_TAG_VALUE="$BINDING" - elif [[ "$BINDING" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$BINDING' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$BINDING" =~ ^.+:[0-9a-fA-F]{7,40}$ ]]; then - GENVM_REF_VALUE="$BINDING" - GENVM_SOURCE_MODE_VALUE="source" - else - echo "::error::Invalid GenVM pin '$BINDING'; expected vX.Y.Z[-suffix] or ':'" - exit 1 - fi - fi - if [[ -n "$GENVM_TAG_VALUE" && ! "$GENVM_TAG_VALUE" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - echo "::error::Invalid GENVM_TAG '$GENVM_TAG_VALUE'; expected vX.Y.Z[-suffix]" - exit 1 - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - if [[ "$GENVM_REF_VALUE" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$GENVM_REF_VALUE' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$GENVM_REF_VALUE" =~ ^(.+):([0-9a-fA-F]{7,40})$ ]]; then - GENVM_REF_BRANCH="${BASH_REMATCH[1]}" - GENVM_REF_COMMIT="$(gh api "repos/genlayerlabs/genvm-manager/commits/${BASH_REMATCH[2]}" --jq .sha)" - GENVM_REF_VALUE="$GENVM_REF_BRANCH:$GENVM_REF_COMMIT" - elif [[ "$GENVM_SOURCE_MODE_VALUE" == "source" ]]; then - GENVM_REF_VALUE="$(gh api "repos/genlayerlabs/genvm-manager/commits/$GENVM_REF_VALUE" --jq .sha)" - else - echo "::error::GENVM_REF must be ':' unless GENVM_SOURCE_MODE=source is explicit" - exit 1 - fi - GENVM_SOURCE_MODE_VALUE="source" - fi - sed -i "s|^GENVM_TAG=.*|GENVM_TAG=\"$GENVM_TAG_VALUE\"|" .env - sed -i "s|^GENVM_REF=.*|GENVM_REF=\"$GENVM_REF_VALUE\"|" .env - sed -i "s|^GENVM_SOURCE_MODE=.*|GENVM_SOURCE_MODE=\"$GENVM_SOURCE_MODE_VALUE\"|" .env - { - echo "GENVM_TAG=$GENVM_TAG_VALUE" - echo "GENVM_REF=$GENVM_REF_VALUE" - echo "GENVM_SOURCE_MODE=$GENVM_SOURCE_MODE_VALUE" - } >> "$GITHUB_ENV" - grep "^GENVM_TAG=" .env - grep "^GENVM_REF=" .env - grep "^GENVM_SOURCE_MODE=" .env - - - name: Configure LLM provider - env: - OPENROUTERAPIKEY: ${{ secrets.OPENROUTERAPIKEY }} - LLM_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - LLM_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: | - # Inject OpenRouter API key - sed -i "/^OPENROUTERAPIKEY *= *''$/d" .env - printf "\nOPENROUTERAPIKEY='$OPENROUTERAPIKEY'\n" >> .env - - # Override validators config with configured provider/model - sed -i "/^VALIDATORS_CONFIG_JSON=/,/^]'/d" .env - printf "\nVALIDATORS_CONFIG_JSON='[\n {\"stake\": 100, \"provider\": \"$LLM_PROVIDER\", \"model\": \"$LLM_MODEL\", \"amount\": 5}\n]'\n" >> .env - - - name: Configure CI environment - run: | - sed -i "s/VITE_FINALITY_WINDOW=\".*\"/VITE_FINALITY_WINDOW=\"10\"/" .env - sed -i "s/COMPOSE_RPC_CPU_LIMIT=\".*\"/COMPOSE_RPC_CPU_LIMIT=\"4\"/" .env - sed -i "s/COMPOSE_WORKER_CPU_LIMIT=\".*\"/COMPOSE_WORKER_CPU_LIMIT=\"4\"/" .env - sed -i "s/COMPOSE_RPC_MEM_LIMIT=\".*\"/COMPOSE_RPC_MEM_LIMIT=\"6gb\"/" .env - sed -i "s/COMPOSE_WORKER_MEM_LIMIT=\".*\"/COMPOSE_WORKER_MEM_LIMIT=\"6gb\"/" .env - sed -i "s/COMPOSE_RPC_MEM_RESERVATION=\".*\"/COMPOSE_RPC_MEM_RESERVATION=\"2gb\"/" .env - sed -i "s/COMPOSE_WORKER_MEM_RESERVATION=\".*\"/COMPOSE_WORKER_MEM_RESERVATION=\"2gb\"/" .env - sed -i "s/JSONRPC_REPLICAS=\".*\"/JSONRPC_REPLICAS=\"1\"/" .env - sed -i "s/CONSENSUS_WORKERS=\".*\"/CONSENSUS_WORKERS=\"3\"/" .env - sed -i "s/MAX_PARALLEL_TXS_PER_WORKER=\".*\"/MAX_PARALLEL_TXS_PER_WORKER=\"2\"/" .env - echo >> .env - if [[ "${{ needs.triggers.outputs.is_pull_request_review_approved }}" == "true" ]]; then - echo "TEST_WITH_MOCK_LLMS=false" >> .env - else - echo "TEST_WITH_MOCK_LLMS=true" >> .env - fi - - # Set up Python + pip early so it overlaps with Docker build - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: pip - cache-dependency-path: | - requirements.txt - requirements.test.txt - backend/requirements.txt - - - name: Install Python dependencies - run: | - pip install -r requirements.txt - pip install -r requirements.test.txt - pip install -r backend/requirements.txt - - - name: Extract GenVM version for cache key - id: genvm - run: | - GENVM_REF_VALUE="${GENVM_REF:-}" - if [[ -z "$GENVM_REF_VALUE" && -f .env ]]; then - GENVM_REF_VALUE="$(sed -n 's/^GENVM_REF=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - GENVM_CACHE_REF="${GENVM_REF_VALUE##*:}" - echo "binding=ref-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - echo "bake_cache=,scope=genvm-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - else - GENVM_TAG_VALUE="${GENVM_TAG:-}" - if [[ -z "$GENVM_TAG_VALUE" && -f .env ]]; then - GENVM_TAG_VALUE="$(sed -n 's/^GENVM_TAG=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -z "$GENVM_TAG_VALUE" ]]; then - echo "::warning::GENVM_TAG not set (this job runs prebuilt images) — using shared 'untagged' precompile cache key that will not invalidate across GenVM upgrades" - GENVM_TAG_VALUE="untagged" - fi - echo "binding=$GENVM_TAG_VALUE" >> "$GITHUB_OUTPUT" - echo "bake_cache=" >> "$GITHUB_OUTPUT" - fi - - - name: Restore GenVM precompile cache - uses: actions/cache@v5 - with: - path: ${{ env.GENVM_CACHE_DIR }} - key: genvm-precompile-${{ runner.os }}-${{ runner.arch }}-${{ steps.genvm.outputs.binding }} - - - name: Prepare GenVM cache directory - run: | - sudo mkdir -p "$GENVM_CACHE_DIR/pc" - sudo chown -R 999:999 "$GENVM_CACHE_DIR" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # Build only backend images with Buildx Bake and enable GHA cache - - name: Build backend images with cache (buildx bake) - uses: docker/bake-action@v6 - with: - files: | - ./docker-compose.yml - targets: | - database-migration - jsonrpc - consensus-worker - set: | - *.cache-from=type=gha${{ steps.genvm.outputs.bake_cache }} - *.cache-to=type=gha,mode=max${{ steps.genvm.outputs.bake_cache }} - database-migration.tags=genlayer-studio-database-migration:latest - jsonrpc.tags=genlayer-studio-jsonrpc:latest - consensus-worker.tags=genlayer-studio-consensus-worker:latest - load: true - - - name: Precompile GenVM cache - timeout-minutes: 30 - run: docker/scripts/precompile_genvm.sh - - # Start services without rebuilding images; CI override provides fast healthcheck timings. - # --wait blocks until all healthchecks pass (jsonrpc /ready + consensus-worker /health). - - name: Run Docker Compose - timeout-minutes: 5 - run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d --no-build --wait database-migration jsonrpc consensus-worker - - - name: Verify services are ready - run: | - echo "Services passed healthchecks, verifying RPC endpoint..." - curl -sS -X POST -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"ping","params":[],"id":1}' \ - http://0.0.0.0:4000/api - - - name: Run tests (parallel) - env: - TEST_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - TEST_PROVIDER_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: gltest --contracts-dir . --default-wait-retries 140 tests/integration/ -svv -n 8 --dist loadgroup --ignore=tests/integration/test_validators.py - - # Validator smoke test runs serial (GenVM LLM module restarts on each CRUD op). - # Full CRUD coverage is in db-sqlalchemy/validators_registry_test.py (<1s). - - name: Run validator smoke test (serial) - env: - TEST_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - TEST_PROVIDER_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: gltest --contracts-dir . --default-wait-retries 140 tests/integration/test_validators.py -svv - - - name: Dump Docker Compose logs - run: docker compose logs - if: failure() - - - name: Shutdown Docker Compose - if: always() - run: docker compose down - - test-gasless: - needs: triggers - if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }} - - runs-on: ubuntu-latest - - env: - PYTHONPATH: ${{ github.workspace }} - COMPOSE_PROFILES: hardhat - GENVM_CACHE_DIR: /tmp/genvm-cache - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Copy .env file - run: cp .env.example .env - - - name: Resolve GenVM binding - env: - GH_TOKEN: ${{ github.token }} - run: | - # Binding model: codebase default < env override. - # third_party/genvm/version (same convention as genlayer-node) pins - # the GenVM this branch's host protocol speaks; it is flipped to the - # release tag at release cut. The e2e pipeline / release train can - # override by injecting GENVM_TAG or GENVM_REF into the environment. - # Exact vX.Y.Z[-suffix] values use release assets. Source pins must be - # explicit : values so builds cannot drift with a branch. - if [[ -n "${GENVM_TAG:-}" && -n "${GENVM_REF:-}" ]]; then - echo "::error::GENVM_TAG and GENVM_REF are both set; they are mutually exclusive" - exit 1 - fi - GENVM_TAG_VALUE="${GENVM_TAG:-}" - GENVM_REF_VALUE="${GENVM_REF:-}" - GENVM_SOURCE_MODE_VALUE="${GENVM_SOURCE_MODE:-}" - if [[ -z "$GENVM_TAG_VALUE" && -z "$GENVM_REF_VALUE" ]]; then - if [[ ! -f third_party/genvm/version ]]; then - echo "::error::third_party/genvm/version is missing and no GENVM_TAG/GENVM_REF env override is set" - exit 1 - fi - BINDING="$(head -n1 third_party/genvm/version | sed -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - if [[ "$BINDING" =~ ^\".*\"$ || "$BINDING" =~ ^\'.*\'$ ]]; then - BINDING="${BINDING:1:${#BINDING}-2}" - BINDING="$(printf '%s' "$BINDING" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - fi - if [[ -z "$BINDING" ]]; then - echo "::error::third_party/genvm/version is empty" - exit 1 - fi - if [[ "$BINDING" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - GENVM_TAG_VALUE="$BINDING" - elif [[ "$BINDING" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$BINDING' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$BINDING" =~ ^.+:[0-9a-fA-F]{7,40}$ ]]; then - GENVM_REF_VALUE="$BINDING" - GENVM_SOURCE_MODE_VALUE="source" - else - echo "::error::Invalid GenVM pin '$BINDING'; expected vX.Y.Z[-suffix] or ':'" - exit 1 - fi - fi - if [[ -n "$GENVM_TAG_VALUE" && ! "$GENVM_TAG_VALUE" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - echo "::error::Invalid GENVM_TAG '$GENVM_TAG_VALUE'; expected vX.Y.Z[-suffix]" - exit 1 - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - if [[ "$GENVM_REF_VALUE" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$GENVM_REF_VALUE' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$GENVM_REF_VALUE" =~ ^(.+):([0-9a-fA-F]{7,40})$ ]]; then - GENVM_REF_BRANCH="${BASH_REMATCH[1]}" - GENVM_REF_COMMIT="$(gh api "repos/genlayerlabs/genvm-manager/commits/${BASH_REMATCH[2]}" --jq .sha)" - GENVM_REF_VALUE="$GENVM_REF_BRANCH:$GENVM_REF_COMMIT" - elif [[ "$GENVM_SOURCE_MODE_VALUE" == "source" ]]; then - GENVM_REF_VALUE="$(gh api "repos/genlayerlabs/genvm-manager/commits/$GENVM_REF_VALUE" --jq .sha)" - else - echo "::error::GENVM_REF must be ':' unless GENVM_SOURCE_MODE=source is explicit" - exit 1 - fi - GENVM_SOURCE_MODE_VALUE="source" - fi - sed -i "s|^GENVM_TAG=.*|GENVM_TAG=\"$GENVM_TAG_VALUE\"|" .env - sed -i "s|^GENVM_REF=.*|GENVM_REF=\"$GENVM_REF_VALUE\"|" .env - sed -i "s|^GENVM_SOURCE_MODE=.*|GENVM_SOURCE_MODE=\"$GENVM_SOURCE_MODE_VALUE\"|" .env - { - echo "GENVM_TAG=$GENVM_TAG_VALUE" - echo "GENVM_REF=$GENVM_REF_VALUE" - echo "GENVM_SOURCE_MODE=$GENVM_SOURCE_MODE_VALUE" - } >> "$GITHUB_ENV" - grep "^GENVM_TAG=" .env - grep "^GENVM_REF=" .env - grep "^GENVM_SOURCE_MODE=" .env - - # TODO: we should also add also heuristai and anthropic keys to the e2e tests and test all providers - - - name: Configure LLM provider - env: - OPENROUTERAPIKEY: ${{ secrets.OPENROUTERAPIKEY }} - LLM_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - LLM_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: | - # Inject OpenRouter API key - sed -i "/^OPENROUTERAPIKEY *= *''$/d" .env - printf "\nOPENROUTERAPIKEY='$OPENROUTERAPIKEY'\n" >> .env - - # Override validators config with configured provider/model - sed -i "/^VALIDATORS_CONFIG_JSON=/,/^]'/d" .env - printf "\nVALIDATORS_CONFIG_JSON='[\n {\"stake\": 100, \"provider\": \"$LLM_PROVIDER\", \"model\": \"$LLM_MODEL\", \"amount\": 5}\n]'\n" >> .env - - - name: Configure CI environment - run: | - sed -i "s/VITE_FINALITY_WINDOW=\".*\"/VITE_FINALITY_WINDOW=\"10\"/" .env - sed -i "s/COMPOSE_RPC_CPU_LIMIT=\".*\"/COMPOSE_RPC_CPU_LIMIT=\"4\"/" .env - sed -i "s/COMPOSE_WORKER_CPU_LIMIT=\".*\"/COMPOSE_WORKER_CPU_LIMIT=\"4\"/" .env - sed -i "s/COMPOSE_RPC_MEM_LIMIT=\".*\"/COMPOSE_RPC_MEM_LIMIT=\"6gb\"/" .env - sed -i "s/COMPOSE_WORKER_MEM_LIMIT=\".*\"/COMPOSE_WORKER_MEM_LIMIT=\"6gb\"/" .env - sed -i "s/COMPOSE_RPC_MEM_RESERVATION=\".*\"/COMPOSE_RPC_MEM_RESERVATION=\"2gb\"/" .env - sed -i "s/COMPOSE_WORKER_MEM_RESERVATION=\".*\"/COMPOSE_WORKER_MEM_RESERVATION=\"2gb\"/" .env - sed -i "s/JSONRPC_REPLICAS=\".*\"/JSONRPC_REPLICAS=\"1\"/" .env - sed -i "s/CONSENSUS_WORKERS=\".*\"/CONSENSUS_WORKERS=\"3\"/" .env - sed -i "s/MAX_PARALLEL_TXS_PER_WORKER=\".*\"/MAX_PARALLEL_TXS_PER_WORKER=\"2\"/" .env - echo >> .env - echo "TEST_WITH_MOCK_LLMS=true" >> .env - - - name: Configure gasless mode - run: | - sed -i "s/^GENLAYER_STUDIO_GEN_PER_TIME_UNIT=.*/GENLAYER_STUDIO_GEN_PER_TIME_UNIT='0'/" .env - sed -i "s/^GENLAYER_STUDIO_STORAGE_UNIT_PRICE=.*/GENLAYER_STUDIO_STORAGE_UNIT_PRICE='0'/" .env - sed -i "s/^GENLAYER_STUDIO_RECEIPT_GAS_PRICE=.*/GENLAYER_STUDIO_RECEIPT_GAS_PRICE='0'/" .env - grep "^GENLAYER_STUDIO_" .env - - # Set up Python + pip early so it overlaps with Docker build - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: pip - cache-dependency-path: | - requirements.txt - requirements.test.txt - backend/requirements.txt - - - name: Install Python dependencies - run: | - bash .github/scripts/install-python-dependencies.sh - - - name: Extract GenVM version for cache key - id: genvm - run: | - GENVM_REF_VALUE="${GENVM_REF:-}" - if [[ -z "$GENVM_REF_VALUE" && -f .env ]]; then - GENVM_REF_VALUE="$(sed -n 's/^GENVM_REF=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - GENVM_CACHE_REF="${GENVM_REF_VALUE##*:}" - echo "binding=ref-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - echo "bake_cache=,scope=genvm-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - else - GENVM_TAG_VALUE="${GENVM_TAG:-}" - if [[ -z "$GENVM_TAG_VALUE" && -f .env ]]; then - GENVM_TAG_VALUE="$(sed -n 's/^GENVM_TAG=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -z "$GENVM_TAG_VALUE" ]]; then - echo "::warning::GENVM_TAG not set (this job runs prebuilt images) — using shared 'untagged' precompile cache key that will not invalidate across GenVM upgrades" - GENVM_TAG_VALUE="untagged" - fi - echo "binding=$GENVM_TAG_VALUE" >> "$GITHUB_OUTPUT" - echo "bake_cache=" >> "$GITHUB_OUTPUT" - fi - - - name: Restore GenVM precompile cache - uses: actions/cache@v5 - with: - path: ${{ env.GENVM_CACHE_DIR }} - key: genvm-precompile-${{ runner.os }}-${{ runner.arch }}-${{ steps.genvm.outputs.binding }} - - - name: Prepare GenVM cache directory - run: | - sudo mkdir -p "$GENVM_CACHE_DIR/pc" - sudo chown -R 999:999 "$GENVM_CACHE_DIR" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - # Build only backend images with Buildx Bake and enable GHA cache - - name: Build backend images with cache (buildx bake) - uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6 - with: - files: | - ./docker-compose.yml - targets: | - database-migration - jsonrpc - consensus-worker - set: | - *.cache-from=type=gha${{ steps.genvm.outputs.bake_cache }} - database-migration.tags=genlayer-studio-database-migration:latest - jsonrpc.tags=genlayer-studio-jsonrpc:latest - consensus-worker.tags=genlayer-studio-consensus-worker:latest - load: true - - - name: Precompile GenVM cache - timeout-minutes: 30 - run: docker/scripts/precompile_genvm.sh - - # Start services without rebuilding images; CI override provides fast healthcheck timings. - # --wait blocks until all healthchecks pass (jsonrpc /ready + consensus-worker /health). - - name: Run Docker Compose - timeout-minutes: 5 - run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d --no-build --wait database-migration jsonrpc consensus-worker - - - name: Verify gasless stack is ready - run: | - echo "Services passed healthchecks, verifying gasless RPC endpoint..." - curl -sS -X POST -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"sim_getFeeConfig","params":[],"id":1}' \ - 0.0.0.0:4000/api | tee /tmp/feeconfig.json - python3 -c "import json;cfg=json.load(open('/tmp/feeconfig.json'))['result'];assert cfg['enabled'] is False, cfg" - - - name: Run gasless tests - env: - TEST_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - TEST_PROVIDER_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: gltest --contracts-dir . --default-wait-retries 140 tests/integration/test_gasless_mode.py -svv -m gasless - - - name: Dump Docker Compose logs - run: docker compose logs - if: failure() - - - name: Shutdown Docker Compose - if: always() - run: docker compose down - - db-integration-test: - needs: triggers - if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }} - - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker layers - uses: actions/cache@v5 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Run Docker Compose - run: docker compose -f tests/db-sqlalchemy/docker-compose.yml --project-directory . run --build --rm tests - - - # hardhat-test: - # needs: triggers - # if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }} - - # runs-on: ubuntu-latest - - # steps: - # - name: Checkout code - # uses: actions/checkout@v4 - - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@v3 - - # - name: Cache Docker layers - # uses: actions/cache@v4 - # with: - # path: /tmp/.buildx-cache - # key: ${{ runner.os }}-buildx-${{ github.sha }} - # restore-keys: | - # ${{ runner.os }}-buildx- - - # - name: Run Docker Compose - # run: docker compose -f tests/hardhat/docker-compose.yml --project-directory . up tests --build --force-recreate --always-recreate-deps diff --git a/.github/workflows/branch-policy.yml b/.github/workflows/branch-policy.yml deleted file mode 100644 index 7759870f7..000000000 --- a/.github/workflows/branch-policy.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Branch Policy - -on: - pull_request: - types: [opened, synchronize, reopened, edited, ready_for_review] - push: - branches: - - "**" - workflow_dispatch: - -permissions: - contents: read - -jobs: - branch-policy: - name: Validate branch policy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Validate branch policy - env: - GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: ./.github/scripts/validate-branch-policy.sh diff --git a/.github/workflows/conventional-commit-pr.yml b/.github/workflows/conventional-commit-pr.yml deleted file mode 100644 index dcb7c34b5..000000000 --- a/.github/workflows/conventional-commit-pr.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: PR Conventional Commit Validation - -on: - pull_request: - types: [opened, synchronize, reopened, edited] - branches: [main] - -jobs: - validate-pr-title: - runs-on: ubuntu-latest - steps: - - name: PR Conventional Commit Validation - uses: ytanikin/PRConventionalCommits@1.3.0 - with: - task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]' - add_label: "false" diff --git a/.github/workflows/docker-build-and-push-all.yml b/.github/workflows/docker-build-and-push-all.yml deleted file mode 100644 index c01f27702..000000000 --- a/.github/workflows/docker-build-and-push-all.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: "Docker build and push to Docker Hub" - -on: - workflow_dispatch: - inputs: - ref: - description: "Git ref to build" - required: true - type: string - image_tag: - description: "Docker image tag to publish" - required: true - type: string - push_latest: - description: "Also publish Docker Hub latest manifests" - required: false - type: boolean - default: true - workflow_call: - inputs: - ref: - required: true - type: string - image_tag: - required: true - type: string - push_latest: - required: false - type: boolean - default: true - secrets: - DOCKERHUB_TOKEN: - required: true - -permissions: - contents: read - -jobs: - jsonrpc: - uses: ./.github/workflows/docker-build-and-push-image.yml - with: - docker_build_context: . - dockerfile: docker/Dockerfile.backend - dockerhub_repo: yeagerai/simulator-jsonrpc - dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} - ref: ${{ inputs.ref }} - image_tag: ${{ inputs.image_tag }} - push_latest: ${{ inputs.push_latest }} - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - frontend: - uses: ./.github/workflows/docker-build-and-push-image.yml - with: - docker_build_context: . - dockerfile: docker/Dockerfile.frontend - dockerhub_repo: yeagerai/simulator-frontend - dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} - ref: ${{ inputs.ref }} - image_tag: ${{ inputs.image_tag }} - push_latest: ${{ inputs.push_latest }} - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - database-migration: - uses: ./.github/workflows/docker-build-and-push-image.yml - with: - docker_build_context: . - dockerfile: docker/Dockerfile.database-migration - dockerhub_repo: yeagerai/simulator-database-migration - dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} - ref: ${{ inputs.ref }} - image_tag: ${{ inputs.image_tag }} - push_latest: ${{ inputs.push_latest }} - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - consensus-worker: - uses: ./.github/workflows/docker-build-and-push-image.yml - with: - docker_build_context: . - dockerfile: docker/Dockerfile.consensus-worker - dockerhub_repo: yeagerai/simulator-consensus-worker - dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} - ref: ${{ inputs.ref }} - image_tag: ${{ inputs.image_tag }} - push_latest: ${{ inputs.push_latest }} - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} - - explorer: - uses: ./.github/workflows/docker-build-and-push-image.yml - with: - docker_build_context: . - dockerfile: docker/Dockerfile.explorer - dockerhub_repo: yeagerai/simulator-explorer - dockerhub_username: ${{ vars.DOCKERHUB_USERNAME }} - ref: ${{ inputs.ref }} - image_tag: ${{ inputs.image_tag }} - push_latest: ${{ inputs.push_latest }} - secrets: - dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/docker-build-and-push-image.yml b/.github/workflows/docker-build-and-push-image.yml deleted file mode 100644 index 720c67dbc..000000000 --- a/.github/workflows/docker-build-and-push-image.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: "Docker build and push to Docker Hub" - -on: - workflow_call: - inputs: - docker_build_context: - required: true - type: string - dockerfile: - required: true - type: string - dockerhub_repo: - required: true - type: string - dockerhub_username: - required: true - type: string - build_args: - required: false - type: string - default: "" - ref: - required: true - type: string - image_tag: - required: true - type: string - push_latest: - required: false - type: boolean - default: true - secrets: - dockerhub_token: - required: true - -permissions: - contents: read - -jobs: - build: - name: Build (${{ matrix.arch }}) - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref }} - fetch-depth: 0 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ inputs.dockerhub_username }} - password: ${{ secrets.dockerhub_token }} - - - name: Create .env file - run: cp .env.example .env - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ${{ inputs.docker_build_context }} - file: ${{ inputs.dockerfile }} - platforms: ${{ matrix.platform }} - push: true - tags: ${{ inputs.dockerhub_repo }}:${{ inputs.image_tag }}-${{ matrix.arch }} - cache-from: type=gha,scope=${{ inputs.dockerhub_repo }}-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ inputs.dockerhub_repo }}-${{ matrix.arch }} - provenance: false - build-args: ${{ inputs.build_args }} - - create-manifest: - name: Create multi-arch manifest - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref }} - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ inputs.dockerhub_username }} - password: ${{ secrets.dockerhub_token }} - - - name: Create and push manifest - env: - DOCKERHUB_REPO: ${{ inputs.dockerhub_repo }} - IMAGE_TAG: ${{ inputs.image_tag }} - PUSH_LATEST: ${{ inputs.push_latest }} - run: | - set -euo pipefail - - # Create manifest for versioned tag - docker manifest create "${DOCKERHUB_REPO}:${IMAGE_TAG}" \ - "${DOCKERHUB_REPO}:${IMAGE_TAG}-amd64" \ - "${DOCKERHUB_REPO}:${IMAGE_TAG}-arm64" - docker manifest push "${DOCKERHUB_REPO}:${IMAGE_TAG}" - - if [[ "${PUSH_LATEST}" != "true" ]]; then - echo "Skipping latest manifest push" - exit 0 - fi - - # Create manifest for latest tag - docker manifest create "${DOCKERHUB_REPO}:latest" \ - "${DOCKERHUB_REPO}:${IMAGE_TAG}-amd64" \ - "${DOCKERHUB_REPO}:${IMAGE_TAG}-arm64" - docker manifest push "${DOCKERHUB_REPO}:latest" diff --git a/.github/workflows/docker-build-test-native-arm.yml b/.github/workflows/docker-build-test-native-arm.yml deleted file mode 100644 index 14b8f1045..000000000 --- a/.github/workflows/docker-build-test-native-arm.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: "Test: Native ARM Docker Build" - -on: - workflow_dispatch: - inputs: - image: - description: 'Image to build (jsonrpc, frontend, consensus-worker, database-migration)' - required: true - default: 'jsonrpc' - type: choice - options: - - jsonrpc - - frontend - - consensus-worker - - database-migration - push: - description: 'Push to DockerHub (use test- prefix)' - required: true - default: false - type: boolean - -permissions: - contents: read - -env: - DOCKERFILE_MAP: '{"jsonrpc":"docker/Dockerfile.backend","frontend":"docker/Dockerfile.frontend","consensus-worker":"docker/Dockerfile.consensus-worker","database-migration":"docker/Dockerfile.database-migration"}' - REPO_MAP: '{"jsonrpc":"yeagerai/simulator-jsonrpc","frontend":"yeagerai/simulator-frontend","consensus-worker":"yeagerai/simulator-consensus-worker","database-migration":"yeagerai/simulator-database-migration"}' - -jobs: - build: - name: Build ${{ inputs.image }} (${{ matrix.platform }}) - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 - steps: - - uses: actions/checkout@v6 - - - name: Set image config - id: config - run: | - echo "dockerfile=${{ fromJson(env.DOCKERFILE_MAP)[inputs.image] }}" >> $GITHUB_OUTPUT - echo "repo=${{ fromJson(env.REPO_MAP)[inputs.image] }}" >> $GITHUB_OUTPUT - - - name: Login to Docker Hub - if: ${{ inputs.push }} - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create .env file - run: cp .env.example .env - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build image - uses: docker/build-push-action@v6 - with: - context: . - file: ${{ steps.config.outputs.dockerfile }} - platforms: ${{ matrix.platform }} - push: ${{ inputs.push }} - tags: ${{ steps.config.outputs.repo }}:test-${{ matrix.arch }} - cache-from: type=gha,scope=${{ inputs.image }}-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=${{ inputs.image }}-${{ matrix.arch }} - - - name: Report build time - run: | - echo "## Build Summary" >> $GITHUB_STEP_SUMMARY - echo "- **Image:** ${{ inputs.image }}" >> $GITHUB_STEP_SUMMARY - echo "- **Platform:** ${{ matrix.platform }}" >> $GITHUB_STEP_SUMMARY - echo "- **Runner:** ${{ matrix.runner }}" >> $GITHUB_STEP_SUMMARY - - create-manifest: - name: Create multi-arch manifest - needs: build - runs-on: ubuntu-latest - if: ${{ inputs.push }} - steps: - - name: Set image config - id: config - run: | - echo "repo=${{ fromJson(env.REPO_MAP)[inputs.image] }}" >> $GITHUB_OUTPUT - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Create and push manifest - run: | - docker manifest create ${{ steps.config.outputs.repo }}:test-multiarch \ - ${{ steps.config.outputs.repo }}:test-amd64 \ - ${{ steps.config.outputs.repo }}:test-arm64 - docker manifest push ${{ steps.config.outputs.repo }}:test-multiarch diff --git a/.github/workflows/e2e-housekeeper.yml b/.github/workflows/e2e-housekeeper.yml deleted file mode 100644 index 50e64a61b..000000000 --- a/.github/workflows/e2e-housekeeper.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: E2E Housekeeper - -# =========================================================================== -# Source-of-truth E2E housekeeper workflow. -# -# This file is BOTH: -# - The workflow that runs in this repo (genlayer-e2e) for genlayer- -# e2e's own cache pool. -# - The file synced byte-identically to every consumer repo as -# `.github/workflows/e2e-housekeeper.yml` (sync-template.yaml owns -# the fan-out). -# -# Mirrors the architecture of e2e-pipeline.yml — one file in the source -# repo, copied verbatim to consumers, no wrapper layer. Adding a new -# scheduled upkeep step (artifact pruning, runner sweep, …) only needs -# editing this file; sync-template.yaml opens a PR in each consumer. -# -# Cache storage is per-repo, so the eviction step runs in the CALLER's -# context: `gh cache list` / `gh cache delete` operate on the caller's -# pool via the inherited GITHUB_TOKEN. The runner's filesystem starts -# empty, so step 1 checks out genlayer-e2e to access the extracted -# `evict-stale-caches.sh` script — github.token in a synced consumer -# context still has cross-org read access on the org's private repos. -# =========================================================================== - -on: - schedule: - - cron: '0 6 * * *' # daily 06:00 UTC - # No inputs: production triggers (schedule today, PR-merged later) - # don't pass them, and the script's defaults (24h idle / 200 page - # cap) are stable. workflow_dispatch stays as a no-arg "run the - # cron now" button — handy in genlayer-e2e while iterating. - workflow_dispatch: - -# Least-privilege. `evict-stale-caches.sh` invokes `gh cache list` -# (read) and `gh cache delete` (write) against the caller's cache pool -# via secrets.GITHUB_TOKEN. The GHA Cache API lives under the actions: -# permission namespace, so deletion requires actions:write. `contents: -# read` covers the checkout that fetches this repo's scripts. -permissions: - actions: write - contents: read - -# Serialize runs so two overlapping firings (manual dispatch + schedule) -# don't both try to delete the same entry. -concurrency: - group: cache-cleanup - cancel-in-progress: false - -jobs: - evict-stale: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - # Pull the extracted script. Explicit `repository:` because — when - # this file is synced to a consumer — actions/checkout's default - # target is the CONSUMER's repo, not genlayer-e2e. github.token - # in the consumer context has org-wide read on private repos so - # the clone works without an App token. - - name: Checkout genlayer-e2e - uses: actions/checkout@v6 - with: - repository: genlayerlabs/genlayer-e2e - token: ${{ github.token }} - - - name: Delete idle caches - env: - # `gh cache delete` consumes GH_TOKEN. github.repository - # resolves to the caller's repo (where the cron fires + the - # cache pool lives). - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: ./taskfiles/housekeeper/scripts/evict-stale-caches.sh --age 24 --limit 200 - - # Companion sweep for artifacts (logs, shard outputs, per-component - # summaries). Same 24h idle window so the two sweeps stay in sync. - # `if: always()` so an early cache-sweep failure doesn't skip the - # artifact pass — they're independent. - - name: Delete idle artifacts - if: always() - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} - run: ./taskfiles/housekeeper/scripts/evict-stale-artifacts.sh --age 24 --limit 1000 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index a684d77ae..000000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,798 +0,0 @@ -# Source-of-truth E2E pipeline workflow. -# -# This file is BOTH: -# - The workflow that runs in this repo (genlayer-e2e) for dispatch / -# local testing. -# - The file synced verbatim to every consumer repo as -# .github/workflows/e2e.yml (sync-template.yaml owns the fan-out). -# -# Consumers don't carry a separate thin caller — keeping the consumer's -# workflow file BYTE-IDENTICAL to this one means: -# - Sidebar tiles stay flat: `acknowledge / register`, `plan / action`, -# `build / discover`, `genlayer-core / shard ... / e2e`, `result` — -# no wrapper-job prefix. A consumer-side thin caller would force -# ` /` on every inner tile (see #386 follow-up where -# deleting e2e-harness.yml restored the flat layout). -# - Behavior changes ship via one sync PR per consumer, instead of -# each consumer hand-editing their wrapper. -# -# Two trigger paths: -# - `issue_comment` — PR comment `/run-e2e [profile] [track] [scope]` -# on the consumer's repo. The acknowledge job's `if:` gates on -# author-association so only members/owners/collaborators can fire -# the pipeline. -# - `workflow_dispatch` — manual / debug in this repo (UI dropdowns). -# Acknowledge is skipped (no PR comment context); plan/build/waves -# run with workflow_dispatch input values. -# -# All internal `uses:` references are cross-repo -# (`genlayerlabs/genlayer-e2e/.github/workflows/X.yml@main`) so the same -# file works whether it lives in this repo or in a synced consumer. -# Feature-branch testing requires sedding `@main` → `@` on the -# inner uses; see feedback_branch_pin_for_testing.md. -# -# Sync-time hack — `on: issue_comment:` injection -# ------------------------------------------------ -# The source-of-truth file in genlayer-e2e does NOT declare an -# `on: issue_comment:` trigger. If it did, this workflow would fire on -# EVERY comment in genlayer-e2e's own PRs (coderabbit, dependabot, …) -# and consume a 3-second skipped "noop" run per comment — visible -# clutter on the Actions list. -# -# `sync-templates.sh` injects the `issue_comment:` trigger block at -# sync time at the `# SYNC_INJECT(issue_comment)` marker below. The -# synced consumer copy gets the trigger; genlayer-e2e's source never -# does. Manual debug here still works via the `workflow_dispatch:` -# trigger which IS declared. -# -# The runtime expressions below (run-name, concurrency, acknowledge's -# `if:`, build/wave `pr-number` / `comment-id` coalesces) all reference -# `github.event_name == 'issue_comment'` and `github.event.comment.*` -# — those evaluate cleanly to false / empty on the workflow_dispatch -# path in genlayer-e2e, so they stay unconditional and work for both -# consumer and source contexts. Only the trigger declaration itself -# is injected. - -name: E2E Pipeline - -# Tag every run with a descriptive title: -# issue_comment /run-e2e: PR # /run-e2e -# workflow_dispatch: E2E Test // -# anything else: noop -# -# Manual dispatch is only available on the source repo (genlayer-e2e -# itself, never on a synced consumer), and is intentionally unrestricted -# — there's no per-actor concurrency group, so a developer can fire -# several test runs side-by-side. Putting profile/track/scope in the -# title makes those runs distinguishable at a glance on the Actions -# list (where the bare run_id was previously opaque). -run-name: >- - ${{ (github.event_name == 'issue_comment' - && startsWith(github.event.comment.body, '/run-e2e') - && format('PR #{0} /run-e2e', github.event.issue.number)) - || (github.event_name == 'workflow_dispatch' - && format('E2E Test {0}/{1}/{2}/{3}', inputs.profile, inputs.track, inputs.scope, inputs.stack)) - || format('noop {0}', github.run_id) }} - -on: - issue_comment: - types: [created] - workflow_dispatch: - inputs: - profile: - description: Profile name (must be a top-level key in profiles.json) - required: false - default: default - type: choice - options: - - default - - testnet - track: - description: Matrix track (must exist as matrix/.yaml) - required: false - default: v0.5 - type: choice - options: - - v0.5 - scope: - description: > - Wave-plan scope filter. `all` runs every wave (default); - `core` runs only wave-1 (genlayer-node / test-core); - `tooling` runs waves 2-4 (SDKs + explorer + testing-suite + wallet). - Out-of-scope waves cascade-skip with reason "scope". - required: false - default: all - type: choice - options: - - all - - core - - tooling - stack: - description: > - Stack-target filter. `all` exercises every declared stack - (default); `dev-env` runs only the genlayer-dev-env variants; - `studio` runs only the GenLayer Studio variants. Components - whose declared `stack-targets` don't include the chosen - stack cascade-skip with reason "stack". - required: false - default: studio # SYNC_INJECT(default_stack) - type: choice - options: - - all - - dev-env - - studio - -permissions: - contents: read - -# /run-e2e triggers share a per-PR group so a new comment cancels the -# previous in-progress run. Other issue_comment events (and -# workflow_dispatch) get a unique group so they never queue behind a -# hung /run-e2e. -concurrency: - group: >- - ${{ (github.event_name == 'issue_comment' - && startsWith(github.event.comment.body, '/run-e2e')) - && format('e2e-{0}-pr-{1}', github.repository, github.event.issue.number) - || format('e2e-noop-{0}', github.run_id) }} - cancel-in-progress: true - -env: - GCP_PROJECT_ID: ${{ vars.GCP_PROJECT_ID || 'devops-infra-428314' }} - GCP_WIF_PROVIDER: ${{ vars.GCP_WIF_PROVIDER || 'projects/795309574007/locations/global/workloadIdentityPools/ci-core-e2e-runner/providers/ci-core-e2e-runner' }} - GCP_SERVICE_ACCOUNT: ${{ vars.GCP_SERVICE_ACCOUNT || 'ci-core-e2e-runner@devops-workload-identities.iam.gserviceaccount.com' }} - GCP_SECRET_APP_CLIENT_ID: ${{ vars.GCP_SECRET_APP_CLIENT_ID || 'ci-core-e2e-runner-app-client-id' }} - GCP_SECRET_APP_PRIVATE_KEY: ${{ vars.GCP_SECRET_APP_PRIVATE_KEY || 'ci-core-e2e-runner-app-private-key' }} - # Suppress notify-outcome's compact PR banner. Detailed results - # table still posted. Remove to re-enable. - E2E_REPORT_SKIP_RESULT: 'true' - -jobs: - # =========================================================================== - # acknowledge — PR-side bookkeeping and comment tokenization. - # - # The `if:` guard lives here (not inside e2e-acknowledge.yml) because - # only the caller can guard on `github.event_name` + comment author - # association before the reusable workflow is even resolved. On - # workflow_dispatch, `github.event.issue` is null → guard false → - # acknowledge is skipped (and the plan job's `if:` opts back in for - # the dispatch path). - # =========================================================================== - acknowledge: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - # PR-state guard: refuse /run-e2e on closed / merged PRs. After a - # PR merges, its head branch is typically deleted, which breaks - # every "content at PR head" fetch (third_party version files, - # target-repo matrix.yaml) — the resolver silently falls through - # to the baseline matrix and downstream cache keys drift off - # whatever the baseline's branch tip happens to be at that moment. - # If you actually need to retest, re-run on main or open a fresh - # PR with the same content. - if: >- - github.event.issue.pull_request - && github.event.issue.state == 'open' - && startsWith(github.event.comment.body, '/run-e2e') - && contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.comment.author_association) - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-acknowledge.yml@main - with: - comment-id: ${{ github.event.comment.id }} - comment-body: ${{ github.event.comment.body }} - issue-number: ${{ github.event.issue.number }} - target-repo: ${{ github.repository }} - server-url: ${{ github.server_url }} - run-id: ${{ github.run_id }} - secrets: inherit - - # =========================================================================== - # plan — pin profile / track / matrix-refs / wave-plans / cache-key. - # Dual-path internally (PR vs dispatch); see e2e-planner.yml. - # - # `if:` opts back in on workflow_dispatch where acknowledge is - # intentionally skipped. PR-comment path with an unauthorized author - # (acknowledge skipped because guard failed) does NOT re-enter here — - # the `github.event_name == 'workflow_dispatch'` clause filters it out. - # =========================================================================== - plan: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - needs: acknowledge - if: | - !cancelled() && - (needs.acknowledge.result == 'success' || - (needs.acknowledge.result == 'skipped' && github.event_name == 'workflow_dispatch')) - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-planner.yml@main - with: - # Coalesce: acknowledge tokens win on the PR-comment path; on - # workflow_dispatch the tokens are empty and we fall back to - # the manual choice inputs. - profile: ${{ needs.acknowledge.outputs.profile-token || inputs.profile }} - track: ${{ needs.acknowledge.outputs.track-token || inputs.track }} - scope: ${{ needs.acknowledge.outputs.scope-token || inputs.scope }} - # SYNC_INJECT(default_stack_fallback) rewrites the literal 'all' on - # consumer copies (e.g. 'dev-env' for genlayer-node), so an - # issue_comment `/run-e2e` with no stack token still routes to - # the per-consumer default — inputs.stack is workflow_dispatch- - # only and resolves empty on the issue_comment path. - stack: ${{ needs.acknowledge.outputs.stack-token || inputs.stack || 'studio' }} # SYNC_INJECT(default_stack_fallback) - target-repo: ${{ github.repository }} - pr-number: ${{ github.event.issue.number || '' }} - comment-id: ${{ github.event.comment.id || '' }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - # acknowledge picks the layered-cache cleavage based on the - # consumer repo. On workflow_dispatch acknowledge is skipped - # and its output is empty — the planner's empty default then - # disables layering (today's pre-Slice-A behaviour). - pre-build-cache: ${{ needs.acknowledge.outputs.pre-build-cache || '' }} - - # =========================================================================== - # build — full stack up + pack to cache. Synthetic PR context on the - # dispatch path: run_id stands in for pr-number; comment-id / - # check-run-id stay empty so downstream PR-facing steps are no-ops. - # =========================================================================== - build: - name: build (dev-env) - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - needs: [acknowledge, plan] - # Without an explicit `if:`, GHA's implicit `success()` would require - # acknowledge to have succeeded — but acknowledge is intentionally - # skipped on the workflow_dispatch path. Mirror the wave jobs and - # gate only on plan succeeding. - # - # Skip when the stack filter omits dev-env. `/run-e2e studio` makes - # every wave row's stack-target='studio' (build-studio handles - # those), so the dev-env bundle isn't needed. Mirrors build-studio's - # inverse gate below. - if: | - !cancelled() && - needs.plan.result == 'success' && - contains(needs.plan.outputs.stack-config, '"target":"dev-env"') - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-build.yml@main - with: - track: ${{ github.ref_name }} - profile: ${{ needs.plan.outputs.profile }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - consensus-ref: ${{ needs.plan.outputs.consensus-ref }} - genlayer-node-ref: ${{ needs.plan.outputs.genlayer-node-ref }} - genlayer-explorer-ref: ${{ needs.plan.outputs.genlayer-explorer-ref }} - harness-ref: ${{ needs.plan.outputs.harness-ref }} - harness-sha: ${{ needs.plan.outputs.harness-sha }} - consensus-sha: ${{ needs.plan.outputs.consensus-sha }} - genlayer-node-sha: ${{ needs.plan.outputs.genlayer-node-sha }} - resolved-shas-json: ${{ needs.plan.outputs.resolved-shas-json }} - build-cache-key: ${{ needs.plan.outputs.build-cache-key }} - full-cache-key: ${{ needs.plan.outputs.full-cache-key }} - pre-build-cache: ${{ needs.plan.outputs.pre-build-cache }} - # Mirrors matrix/.yaml shape ({core, harness, tooling}). The - # conclusion job's Emit build summary step parses this with jq - # to render the full component Refs sub-list in the Execution - # block. Trailing JSON commas would be invalid — keep the same - # field set as the matrix file. - matrix-json: >- - {"core":{"genlayer-node":"${{ needs.plan.outputs.genlayer-node-ref }}","genlayer-consensus":"${{ needs.plan.outputs.consensus-ref }}","genvm":"${{ needs.plan.outputs.genvm-version }}"},"harness":{"genlayer-dev-env":"${{ needs.plan.outputs.harness-ref }}"},"tooling":{"genlayer-js":"${{ needs.plan.outputs.genlayer-js-ref }}","genlayer-py":"${{ needs.plan.outputs.genlayer-py-ref }}","genlayer-cli":"${{ needs.plan.outputs.genlayer-cli-ref }}","genlayer-studio":"${{ needs.plan.outputs.genlayer-studio-ref }}","genlayer-explorer":"${{ needs.plan.outputs.genlayer-explorer-ref }}","genlayer-testing-suite":"${{ needs.plan.outputs.genlayer-testing-suite-ref }}","genvm-linter":"${{ needs.plan.outputs.genvm-linter-ref }}","genlayer-wallet":"${{ needs.plan.outputs.genlayer-wallet-ref }}"}} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - github-retry-max: ${{ needs.plan.outputs.github-retry-max }} - github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} - github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} - - # =========================================================================== - # build-studio — bring up a Studio stack and pack it for Studio-target shards. - # Runs in parallel with the dev-env build. Devenv-target wave rows continue - # to consume the existing full-cache-key; Studio-target rows restore the - # per-run Studio bundle produced here. - # =========================================================================== - build-studio: - name: build (studio) - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - needs: [acknowledge, plan] - if: | - !cancelled() && - needs.plan.result == 'success' && - contains(needs.plan.outputs.stack-config, '"target":"studio"') - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-build-studio.yml@main - with: - track: ${{ github.ref_name }} - profile: ${{ needs.plan.outputs.profile }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} - studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - - # =========================================================================== - # Wave jobs — cascade pattern with sentinel-as-skip. Each wave matrix - # comes from `needs.plan.outputs.wave-plans` (one JSON object folding - # every per-wave plan array). - # =========================================================================== - wave-1: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - # `name:` overrides GHA's default `wave-1 (component, test-task, …)` - # matrix-tuple display. Reads matrix.job-name (resolved from - # components.yaml's `job-name` field, falling back to the - # component key) so the listing reads e.g. `genlayer-core / shard - # (shard-1, true) / e2e`. - # - # When build fails, append "(skipped - build fails)" to the tile so - # the UI attributes the skip to its root-cause layer. The - # `matrix.component != 'none'` guard prevents existing sentinel rows - # (e.g., scope-filtered or impacted-set empty waves) from being - # relabelled — those keep their baked-in "(skipped - manual)" / - # "(skipped - scope)" / "(skipped - no scenarios)" label unchanged. - # Waves 2-4 mirror this pattern with extra upstream-wave clauses - # reading `needs.wave-K.outputs.failure-label` (the per-component - # layer tag emitted by e2e-run.yml when that wave's run failed). - name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio] - # No `build-status == 'success'` gate — when build fails we want - # wave-1 to RUN (so the `name:` expression evaluates and tiles - # render cleanly) but no-op via the sentinel-component override - # below. Same mechanism as the existing wave-2/3/4 cascade. - # - # `!cancelled() &&` bypasses GHA's implicit needs-failure-cascade: - # when an upstream `needs:` job (here, `build`) fails, GHA defaults - # to auto-skipping downstream jobs unless their `if:` explicitly - # opts in via `always()` / `!cancelled()` / `failure()`. A skipped - # job does NOT have its `name:` expression evaluated, so without - # this prefix wave-1's tile shows the raw `${{ ... }}` text on - # build failure. Waves 2-4 already carry the same prefix. - # - # resolve-components / build-wave-plans emit a sentinel-row plan - # (component='none') when no impacted component maps to this wave, - # so the matrix always has at least one row and GHA can render - # `${{ matrix.job-name }}` for the placeholder tile (per - # actions/runner#1985, the unresolved name expression needs a - # matrix row to bind to). The sentinel's empty features-source / - # tags / test-task naturally cascade through e2e-run.yml's - # allocate → shard → conclusion chain to a no-op success — see - # resolve-components/action.yml for the full cascade explanation. - if: | - !cancelled() && - needs.plan.result == 'success' - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.plan.outputs.wave-plans)['wave-1'] }} - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-run.yml@main - with: - # Cascade override: if build did NOT produce a clean verdict, - # force the sentinel path so wave-1 no-ops cleanly. Same - # mechanism waves 2-4 use for upstream-wave failures — see - # e2e-run.yml's sentinel branches. - component: ${{ ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) && 'none' || matrix.component }} - track: ${{ github.ref_name }} - job-name: ${{ matrix.job-name || matrix.component }} - stack-target: ${{ matrix.stack-target || 'dev-env' }} - setup-task: ${{ matrix.setup-task }} - test-task: ${{ matrix.test-task }} - tags: ${{ matrix.tags }} - split: ${{ matrix.split }} - features-source: ${{ matrix.features-source }} - retry: ${{ matrix.retry }} - max-shard-split: ${{ matrix.max-shard-split || 0 }} - failure-tag: ${{ matrix.failure-tag || '' }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - genlayer-node-ref: ${{ needs.plan.outputs.genlayer-node-ref }} - consensus-ref: ${{ needs.plan.outputs.consensus-ref }} - genlayer-js-ref: ${{ needs.plan.outputs.genlayer-js-ref }} - genlayer-py-ref: ${{ needs.plan.outputs.genlayer-py-ref }} - genlayer-cli-ref: ${{ needs.plan.outputs.genlayer-cli-ref }} - genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} - genlayer-explorer-ref: ${{ needs.plan.outputs.genlayer-explorer-ref }} - genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} - genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} - genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} - harness-ref: ${{ needs.plan.outputs.harness-ref }} - harness-sha: ${{ needs.plan.outputs.harness-sha }} - cache-key: ${{ needs.plan.outputs.full-cache-key }} - studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - github-retry-max: ${{ needs.plan.outputs.github-retry-max }} - github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} - github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} - - wave-2: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - # Cascade-skip label rules (walked in order, first match wins): - # 1. build failed → "(skipped - build fails)" - # 2. wave-1's failure-label is non-empty (e.g. "core") → - # "(skipped - {label} fails)" - # 3. wave-1 failed but emitted no tag (failure-label empty) → - # generic "(skipped - fails)" - # else → matrix.job-name (real run or sentinel-baked label) - # - # `failure-label` is e2e-run.yml's per-component output, populated - # from `inputs.failure-tag` only when the run's test-conclusion is - # 'failure'. Empty otherwise — so a successful or sentinel-skipped - # wave doesn't carry a stale tag forward. - name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-1.result == 'failure') && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1] - # No cascade gate — wave-2 always runs when plan is OK. If - # build or wave-1 failed, with.component is overridden to 'none' - # below, tripping e2e-run.yml's sentinel path (allocate/shard/ - # check-run/retry/cleanup-artifacts skip; conclusion emits - # 'skipped'). Same flow as manual skip:true. - if: | - !cancelled() && - needs.plan.result == 'success' - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.plan.outputs.wave-plans)['wave-2'] }} - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-run.yml@main - with: - # Cascade override: force sentinel path when any upstream layer - # (build / wave-1) failed. `.result == 'failure'` is the - # reliable trip signal — failure-label is only used for the - # tile label content above, not the trip decision. - component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} - job-name: ${{ matrix.job-name || matrix.component }} - stack-target: ${{ matrix.stack-target || 'dev-env' }} - setup-task: ${{ matrix.setup-task }} - test-task: ${{ matrix.test-task }} - tags: ${{ matrix.tags }} - split: ${{ matrix.split }} - features-source: ${{ matrix.features-source }} - retry: ${{ matrix.retry }} - max-shard-split: ${{ matrix.max-shard-split || 0 }} - failure-tag: ${{ matrix.failure-tag || '' }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - genlayer-node-ref: ${{ needs.plan.outputs.genlayer-node-ref }} - consensus-ref: ${{ needs.plan.outputs.consensus-ref }} - genlayer-js-ref: ${{ needs.plan.outputs.genlayer-js-ref }} - genlayer-py-ref: ${{ needs.plan.outputs.genlayer-py-ref }} - genlayer-cli-ref: ${{ needs.plan.outputs.genlayer-cli-ref }} - genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} - genlayer-explorer-ref: ${{ needs.plan.outputs.genlayer-explorer-ref }} - genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} - genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} - genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} - harness-ref: ${{ needs.plan.outputs.harness-ref }} - harness-sha: ${{ needs.plan.outputs.harness-sha }} - cache-key: ${{ needs.plan.outputs.full-cache-key }} - studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - github-retry-max: ${{ needs.plan.outputs.github-retry-max }} - github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} - github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} - - wave-3: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - # See wave-2 for the cascade-skip label semantics. Walk order - # (first match wins): build → wave-1 failure-label → wave-2 - # failure-label → generic-fallback. The `.result == 'failure'` - # fallback catches the rare case where an upstream wave failed - # but didn't emit a failure-tag (e.g. internal job-level error - # before conclusion ran). - name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-2.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-2.outputs.failure-label) || (matrix.component != 'none' && (needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure')) && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1, wave-2] - # No cascade gate — wave-3 always runs. If build or any upstream - # wave failed, with.component is overridden to 'none' below, - # tripping e2e-run.yml's sentinel path (no GCE provisioned, - # conclusion emits 'skipped'). See wave-2 for the full rationale. - if: | - !cancelled() && - needs.plan.result == 'success' - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.plan.outputs.wave-plans)['wave-3'] }} - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-run.yml@main - with: - # Cascade override: force sentinel path when any upstream layer - # (build / wave-1 / wave-2) failed. Uses `.result == 'failure'` - # for reliability — failure-label is only consumed for the - # tile label content above, not the trip decision. - component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} - job-name: ${{ matrix.job-name || matrix.component }} - stack-target: ${{ matrix.stack-target || 'dev-env' }} - setup-task: ${{ matrix.setup-task }} - test-task: ${{ matrix.test-task }} - tags: ${{ matrix.tags }} - split: ${{ matrix.split }} - features-source: ${{ matrix.features-source }} - retry: ${{ matrix.retry }} - max-shard-split: ${{ matrix.max-shard-split || 0 }} - failure-tag: ${{ matrix.failure-tag || '' }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - genlayer-node-ref: ${{ needs.plan.outputs.genlayer-node-ref }} - consensus-ref: ${{ needs.plan.outputs.consensus-ref }} - genlayer-js-ref: ${{ needs.plan.outputs.genlayer-js-ref }} - genlayer-py-ref: ${{ needs.plan.outputs.genlayer-py-ref }} - genlayer-cli-ref: ${{ needs.plan.outputs.genlayer-cli-ref }} - genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} - genlayer-explorer-ref: ${{ needs.plan.outputs.genlayer-explorer-ref }} - genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} - genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} - genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} - harness-ref: ${{ needs.plan.outputs.harness-ref }} - harness-sha: ${{ needs.plan.outputs.harness-sha }} - cache-key: ${{ needs.plan.outputs.full-cache-key }} - studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - github-retry-max: ${{ needs.plan.outputs.github-retry-max }} - github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} - github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} - - wave-4: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - # See wave-2 for the cascade-skip label semantics. Walk order - # (first match wins): build → wave-1 → wave-2 → wave-3 failure- - # label, then generic-fallback via `.result == 'failure'`. - name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-2.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-2.outputs.failure-label) || (matrix.component != 'none' && needs.wave-3.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-3.outputs.failure-label) || (matrix.component != 'none' && (needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure' || needs.wave-3.result == 'failure')) && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1, wave-2, wave-3] - # No cascade gate — wave-4 always runs. See wave-2 for the - # cascade-as-sentinel rationale. - if: | - !cancelled() && - needs.plan.result == 'success' - strategy: - fail-fast: false - matrix: - include: ${{ fromJson(needs.plan.outputs.wave-plans)['wave-4'] }} - uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-run.yml@main - with: - # Cascade override: force sentinel path when any upstream layer - # (build / wave-1 / wave-2 / wave-3) failed. - component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure' || needs.wave-3.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} - job-name: ${{ matrix.job-name || matrix.component }} - stack-target: ${{ matrix.stack-target || 'dev-env' }} - setup-task: ${{ matrix.setup-task }} - test-task: ${{ matrix.test-task }} - tags: ${{ matrix.tags }} - split: ${{ matrix.split }} - features-source: ${{ matrix.features-source }} - retry: ${{ matrix.retry }} - max-shard-split: ${{ matrix.max-shard-split || 0 }} - failure-tag: ${{ matrix.failure-tag || '' }} - genvm-version: ${{ needs.plan.outputs.genvm-version }} - genlayer-node-ref: ${{ needs.plan.outputs.genlayer-node-ref }} - consensus-ref: ${{ needs.plan.outputs.consensus-ref }} - genlayer-js-ref: ${{ needs.plan.outputs.genlayer-js-ref }} - genlayer-py-ref: ${{ needs.plan.outputs.genlayer-py-ref }} - genlayer-cli-ref: ${{ needs.plan.outputs.genlayer-cli-ref }} - genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} - genlayer-explorer-ref: ${{ needs.plan.outputs.genlayer-explorer-ref }} - genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} - genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} - genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} - harness-ref: ${{ needs.plan.outputs.harness-ref }} - harness-sha: ${{ needs.plan.outputs.harness-sha }} - cache-key: ${{ needs.plan.outputs.full-cache-key }} - studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} - github-retry-max: ${{ needs.plan.outputs.github-retry-max }} - github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} - github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} - - # =========================================================================== - # result — final guard that makes the workflow's conclusion reflect - # the REAL outcome. Mirrors `plan` at the start (plan → build → - # waves → result). - # - # Per-wave verdict comes from each wave's `test-conclusion` output - # (sourced from e2e-run.yml's `conclusion` job), which reflects retry - # recovery — so a first-try shard failure that retry recovered - # counts as 'success'. When test-conclusion is empty (wave was - # skipped or never reached the conclusion job), fall back to GHA's - # `.result` for the rough verdict. - # =========================================================================== - result: - permissions: - contents: read - id-token: write - issues: write - checks: write - actions: write - pull-requests: write - needs: - - acknowledge - - plan - - build - - build-studio - - wave-1 - - wave-2 - - wave-3 - - wave-4 - # `always() && plan.result != 'skipped'` rather than bare `always()`: - # noop issue_comments (any non-/run-e2e comment in this repo, since - # the workflow lives at `on: issue_comment`) skip acknowledge → plan - # → build → waves all the way down. Without this guard, result still - # runs, and its env block resolves - # `toJson(fromJson(needs.plan.outputs.wave-plans)['wave-N'])` against - # plan's empty output → fromJson('') fails the template at - # job-start (see run 26223668789). Letting result skip on - # plan==skipped keeps the noop pipeline clean — `acknowledge / plan / - # build / waves / result` all skipped, no red marker. - if: always() && needs.plan.result != 'skipped' - runs-on: ubuntu-latest - steps: - # App token for the final notify-outcome — completing the - # check-run + swapping the 👀 reaction requires the App token - # (consumer's GITHUB_TOKEN can't update a check-run the App - # created). Cross-repo `uses:` because no checkout has run yet. - - name: Generate GitHub App token - id: app-token - uses: genlayerlabs/genlayer-e2e/.github/actions/gcp-app-token@main - - # App token, not the default github.token: when this workflow is - # synced to a consumer, github.token is the CONSUMER's repo-scoped - # token and can't read the private genlayer-e2e repo (exit 128 - # auth failure on the actions/checkout step — observed on - # genlayer-node run 26240246363). - - uses: actions/checkout@v6 - with: - repository: genlayerlabs/genlayer-e2e - ref: main - token: ${{ steps.app-token.outputs.token }} - - # Per-(component, stack-target) verdict artifacts uploaded by - # each e2e-run.yml conclusion job. One artifact per matrix row - # — pattern flattens them into a single directory keyed by - # artifact-name (= component). Aggregate reads this directory - # to build per-wave verdicts without relying on - # `needs.wave-N.outputs.test-conclusion` (which is racy under - # matrix fan-out — last write wins). - - name: Download test-conclusion artifacts - id: download-conclusions - continue-on-error: true - uses: actions/download-artifact@v8 - with: - pattern: e2e-test-conclusion-*-pr${{ github.event.issue.number || github.run_id }} - path: /tmp/test-conclusions - - - name: Aggregate outcomes - id: aggregate - # `continue-on-error: true` so a failed aggregation (exit 1 on - # any wave failure) doesn't shortcut the notify-outcome step - # below. The Enforce step at the end re-propagates failure to - # the workflow conclusion. - continue-on-error: true - env: - PLAN_RESULT: ${{ needs.plan.result }} - # BUILD_STATUS is the AND of the two stack-scoped builds, - # but a build that wasn't requested (its stack filtered out - # by /run-e2e ) counts as success — its job is - # `skipped`, not `failure`. The contains() checks gate each - # build's contribution by whether its stack-target appears - # in the wave-plans output. - BUILD_STATUS: ${{ ((!contains(needs.plan.outputs.stack-config, '"target":"dev-env"') || needs.build.outputs.build-status == 'success') && (!contains(needs.plan.outputs.stack-config, '"target":"studio"') || needs.build-studio.outputs.build-status == 'success')) && 'success' || 'failure' }} - BUILD_RESULT: ${{ format('dev-env={0}, studio={1}', needs.build.result, needs.build-studio.result) }} - # Classifier-emitted stage + detail surfaced in the build row's - # Notes column when BUILD_STATUS != success. Empty when build - # was a cache-hit (no execute job ran) — the aggregate script - # falls back to the legacy verdict-word render. - BUILD_STAGE: ${{ (contains(needs.plan.outputs.stack-config, '"target":"studio"') && needs.build-studio.outputs.build-status != 'success') && 'build:studio' || needs.build.outputs.build-stage || '' }} - BUILD_DETAIL: ${{ (contains(needs.plan.outputs.stack-config, '"target":"studio"') && needs.build-studio.outputs.build-status != 'success') && 'Studio build failed' || needs.build.outputs.build-detail || '' }} - WAVE_1_RESULT: ${{ needs.wave-1.result }} - WAVE_1_PLAN: ${{ toJson(fromJson(needs.plan.outputs.wave-plans)['wave-1']) }} - WAVE_2_RESULT: ${{ needs.wave-2.result }} - WAVE_2_PLAN: ${{ toJson(fromJson(needs.plan.outputs.wave-plans)['wave-2']) }} - WAVE_3_RESULT: ${{ needs.wave-3.result }} - WAVE_3_PLAN: ${{ toJson(fromJson(needs.plan.outputs.wave-plans)['wave-3']) }} - WAVE_4_RESULT: ${{ needs.wave-4.result }} - WAVE_4_PLAN: ${{ toJson(fromJson(needs.plan.outputs.wave-plans)['wave-4']) }} - # Per-(component, stack-target) verdict directory (one file - # per artifact, name == component, content ∈ {success, - # failure, skipped}). Replaces the legacy single-value - # WAVE_N_TEST_CONCLUSION env which collapses under matrix - # fan-out (last write wins) — see Download test-conclusion - # artifacts step above. PR_NUMBER lets the script construct - # the exact artifact path; on workflow_dispatch (no PR) the - # run_id stands in, matching the wave jobs' pr-number input. - CONCLUSIONS_DIR: /tmp/test-conclusions - PR_NUMBER: ${{ github.event.issue.number || github.run_id }} - run: ./taskfiles/runner/scripts/aggregate-wave-outcomes.sh - - # Final notify-outcome — flips 👀 → 🚀 (overall pass) or 👎 - # (any wave failed), completes the "E2E Tests" check-run, and - # (with E2E_REPORT_SKIP_RESULT="true" suppressing the compact - # banner) skips posting a duplicate PR comment. Per-component - # PR comments were already posted by each wave's e2e-report.sh. - # Gated on check-run-id presence so workflow_dispatch (no - # acknowledge → no check-run) skips silently. - # - # Also skip when plan failed: the planner's own "Notify resolve - # failure" step already posted the structured error message - # (e.g. "Malformed Depends-On line: ...") and updated the - # check-run + reaction. The aggregate's failed-pipeline banner - # here would be a half-empty table that just says "plan failed" - # — no extra signal, and it buries the specific error message - # under a generic-looking second comment. - - name: Notify final outcome - if: always() && needs.acknowledge.outputs.check-run-id != '' && needs.plan.result != 'failure' - continue-on-error: true - uses: genlayerlabs/genlayer-e2e/.github/actions/notify-outcome@main - with: - outcome: ${{ steps.aggregate.outcome == 'success' && 'success' || 'failure' }} - check-run-title: 'E2E Tests' - repo: ${{ github.repository }} - comment-id: ${{ github.event.comment.id }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id }} - pr-number: ${{ github.event.issue.number }} - github-token: ${{ steps.app-token.outputs.token }} - - # Re-propagate the aggregate's exit status to the workflow - # conclusion. Without this, the result job would always succeed - # (because aggregate has `continue-on-error: true`) and the - # whole pipeline would render as green even on real failures. - - name: Enforce overall outcome - if: steps.aggregate.outcome == 'failure' - run: | - echo "::error::Pipeline failed — see ::error:: lines in Aggregate outcomes step" - exit 1 diff --git a/.github/workflows/fast-forward-main.yaml b/.github/workflows/fast-forward-main.yaml deleted file mode 100644 index 688e997a2..000000000 --- a/.github/workflows/fast-forward-main.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: Fast-forward main - -# main is the static/default branch for GitHub UX and tools that assume a -# stable default branch. It is not the integration target. On each push to the -# configured active dev branch, fast-forward main to that commit. - -on: - push: - branches: ["**"] - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: fast-forward-main-${{ github.repository }} - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - fast-forward: - if: github.ref_type == 'branch' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Fast-forward main to active dev branch - run: | - set -euo pipefail - - active_branch="$(tr -d '[:space:]' < support/ci/ACTIVE_DEV_BRANCH)" - if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then - echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" - exit 1 - fi - - if [[ "${GITHUB_REF_NAME}" != "${active_branch}" ]]; then - echo "Push was to ${GITHUB_REF_NAME}; active dev branch is ${active_branch}. Nothing to do." - exit 0 - fi - - if git ls-remote --exit-code --heads origin main >/dev/null 2>&1; then - git fetch origin main - if ! git merge-base --is-ancestor origin/main HEAD; then - echo "::error::main has diverged from ${active_branch}; refusing non-fast-forward update" - exit 1 - fi - else - echo "main does not exist yet; creating it at ${GITHUB_SHA}." - fi - - git push origin "HEAD:refs/heads/main" diff --git a/.github/workflows/frontend-unit-tests.yml b/.github/workflows/frontend-unit-tests.yml deleted file mode 100644 index 71f7bb0e3..000000000 --- a/.github/workflows/frontend-unit-tests.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: "Frontend Unit Tests" - -on: - workflow_call: - secrets: - codecov_token: - required: true - -jobs: - frontend: - runs-on: ubuntu-latest - defaults: - run: - working-directory: frontend - steps: - - uses: actions/checkout@v6 - - name: Use Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - - name: Copy examples to assets - run: cp -r examples frontend/src/assets/examples - working-directory: . - - - run: npm ci - - run: npm run type-check - - run: npm run coverage - - - name: Upload coverage reports to Codecov - - uses: codecov/codecov-action@v5.5.2 - with: - verbose: true - token: ${{ secrets.codecov_token }} - fail_ci_if_error: false - directory: frontend/coverage diff --git a/.github/workflows/genvm-lint.yml b/.github/workflows/genvm-lint.yml deleted file mode 100644 index 42ae56647..000000000 --- a/.github/workflows/genvm-lint.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: GenVM Contract Lint - -on: - pull_request: - paths: - - "examples/contracts/**" - - "tests/load/contracts/**" - - "tests/direct/contracts/**" - - "tests/integration/icontracts/contracts/**" - push: - branches: - - main - paths: - - "examples/contracts/**" - - "tests/load/contracts/**" - - "tests/direct/contracts/**" - - "tests/integration/icontracts/contracts/**" - -jobs: - lint-contracts: - name: Lint Intelligent Contracts - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install genvm-linter - run: pip install genvm-linter - - # Contracts pinned to the genvm-main runner use the new SDK layout, - # which released genvm-linter (<= 0.11.0) cannot load yet - # ("No module named 'genlayer.py'"). Skip them until a linter release - # supports it (genvm-linter dxp-694), then drop this filter. - - name: Lint example contracts - run: | - failed=0 - for f in examples/contracts/*.py; do - echo "::group::$f" - if grep -q "py-genlayer:1zr6nqk597d97kg0dyxg0shhrykx5v02zjgnyrajapy4wlqvfvwh" "$f"; then - echo "SKIP: pinned to the genvm-main runner (unsupported by released genvm-linter)" - elif ! genvm-lint check "$f"; then - failed=1 - fi - echo "::endgroup::" - done - exit $failed - - - name: Lint test contracts - run: | - failed=0 - for f in tests/load/contracts/*.py tests/direct/contracts/*.py; do - [ -f "$f" ] || continue - echo "::group::$f" - if grep -q "py-genlayer:1zr6nqk597d97kg0dyxg0shhrykx5v02zjgnyrajapy4wlqvfvwh" "$f"; then - echo "SKIP: pinned to the genvm-main runner (unsupported by released genvm-linter)" - elif ! genvm-lint check "$f"; then - failed=1 - fi - echo "::endgroup::" - done - exit $failed diff --git a/.github/workflows/load-test-oha.yml b/.github/workflows/load-test-oha.yml deleted file mode 100644 index 25ea17e1e..000000000 --- a/.github/workflows/load-test-oha.yml +++ /dev/null @@ -1,603 +0,0 @@ -name: Load Test with oha CLI - -on: - workflow_call: - inputs: - oha-version: - description: 'Version of oha CLI to use for load testing' - required: false - type: string - default: 'v1.9.0' - pull_request: - types: - - opened - - labeled - - synchronize - pull_request_review: - types: - - submitted - - edited - -jobs: - triggers: - name: Get Triggers - runs-on: ubuntu-latest - outputs: - is_pull_request_opened: ${{ github.event_name == 'pull_request' && github.event.action == 'opened'}} - is_pull_request_review_approved: ${{ github.event_name == 'pull_request_review' && github.event.review.state == 'APPROVED'}} - is_pull_request_labeled_with_run_tests: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-tests')}} - steps: - - run: true - - load-test: - name: Load Tests - needs: triggers - if: ${{ needs.triggers.outputs.is_pull_request_opened == 'true' || needs.triggers.outputs.is_pull_request_review_approved == 'true' || needs.triggers.outputs.is_pull_request_labeled_with_run_tests == 'true' }} - runs-on: ubuntu-latest - - env: - GENVM_CACHE_DIR: /tmp/genvm-cache - - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Install oha - run: | - sudo apt-get update - sudo apt-get install -y curl jq - # Use the input version or default - OHA_VERSION="${{ inputs.oha-version || 'v1.9.0' }}" - echo "Installing oha version: $OHA_VERSION" - - # Try to download oha - if ! curl -sSfL "https://github.com/hatoo/oha/releases/download/${OHA_VERSION}/oha-linux-amd64" -o oha; then - echo "Failed to download oha version $OHA_VERSION, trying latest..." - # Fallback to latest version - LATEST_VERSION=$(curl -s https://api.github.com/repos/hatoo/oha/releases/latest | jq -r '.tag_name') - echo "Using latest version: $LATEST_VERSION" - curl -sSfL "https://github.com/hatoo/oha/releases/download/${LATEST_VERSION}/oha-linux-amd64" -o oha - fi - - chmod +x oha - sudo mv oha /usr/local/bin - oha --version - - - name: Copy .env file - run: cp .env.example .env - - - name: Resolve GenVM binding - env: - GH_TOKEN: ${{ github.token }} - run: | - # Binding model: codebase default < env override. - # third_party/genvm/version (same convention as genlayer-node) pins - # the GenVM this branch's host protocol speaks; it is flipped to the - # release tag at release cut. The e2e pipeline / release train can - # override by injecting GENVM_TAG or GENVM_REF into the environment. - # Exact vX.Y.Z[-suffix] values use release assets. Source pins must be - # explicit : values so builds cannot drift with a branch. - if [[ -n "${GENVM_TAG:-}" && -n "${GENVM_REF:-}" ]]; then - echo "::error::GENVM_TAG and GENVM_REF are both set; they are mutually exclusive" - exit 1 - fi - GENVM_TAG_VALUE="${GENVM_TAG:-}" - GENVM_REF_VALUE="${GENVM_REF:-}" - GENVM_SOURCE_MODE_VALUE="${GENVM_SOURCE_MODE:-}" - if [[ -z "$GENVM_TAG_VALUE" && -z "$GENVM_REF_VALUE" ]]; then - if [[ ! -f third_party/genvm/version ]]; then - echo "::error::third_party/genvm/version is missing and no GENVM_TAG/GENVM_REF env override is set" - exit 1 - fi - BINDING="$(head -n1 third_party/genvm/version | sed -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - if [[ "$BINDING" =~ ^\".*\"$ || "$BINDING" =~ ^\'.*\'$ ]]; then - BINDING="${BINDING:1:${#BINDING}-2}" - BINDING="$(printf '%s' "$BINDING" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - fi - if [[ -z "$BINDING" ]]; then - echo "::error::third_party/genvm/version is empty" - exit 1 - fi - if [[ "$BINDING" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - GENVM_TAG_VALUE="$BINDING" - elif [[ "$BINDING" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$BINDING' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$BINDING" =~ ^.+:[0-9a-fA-F]{7,40}$ ]]; then - GENVM_REF_VALUE="$BINDING" - GENVM_SOURCE_MODE_VALUE="source" - else - echo "::error::Invalid GenVM pin '$BINDING'; expected vX.Y.Z[-suffix] or ':'" - exit 1 - fi - fi - if [[ -n "$GENVM_TAG_VALUE" && ! "$GENVM_TAG_VALUE" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then - echo "::error::Invalid GENVM_TAG '$GENVM_TAG_VALUE'; expected vX.Y.Z[-suffix]" - exit 1 - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - if [[ "$GENVM_REF_VALUE" == *-dev ]]; then - echo "::error::Bare GenVM branch pin '$GENVM_REF_VALUE' is not supported; pin an exact vX.Y.Z release or ':'" - exit 1 - elif [[ "$GENVM_REF_VALUE" =~ ^(.+):([0-9a-fA-F]{7,40})$ ]]; then - GENVM_REF_BRANCH="${BASH_REMATCH[1]}" - GENVM_REF_COMMIT="$(gh api "repos/genlayerlabs/genvm-manager/commits/${BASH_REMATCH[2]}" --jq .sha)" - GENVM_REF_VALUE="$GENVM_REF_BRANCH:$GENVM_REF_COMMIT" - elif [[ "$GENVM_SOURCE_MODE_VALUE" == "source" ]]; then - GENVM_REF_VALUE="$(gh api "repos/genlayerlabs/genvm-manager/commits/$GENVM_REF_VALUE" --jq .sha)" - else - echo "::error::GENVM_REF must be ':' unless GENVM_SOURCE_MODE=source is explicit" - exit 1 - fi - GENVM_SOURCE_MODE_VALUE="source" - fi - sed -i "s|^GENVM_TAG=.*|GENVM_TAG=\"$GENVM_TAG_VALUE\"|" .env - sed -i "s|^GENVM_REF=.*|GENVM_REF=\"$GENVM_REF_VALUE\"|" .env - sed -i "s|^GENVM_SOURCE_MODE=.*|GENVM_SOURCE_MODE=\"$GENVM_SOURCE_MODE_VALUE\"|" .env - { - echo "GENVM_TAG=$GENVM_TAG_VALUE" - echo "GENVM_REF=$GENVM_REF_VALUE" - echo "GENVM_SOURCE_MODE=$GENVM_SOURCE_MODE_VALUE" - } >> "$GITHUB_ENV" - grep "^GENVM_TAG=" .env - grep "^GENVM_REF=" .env - grep "^GENVM_SOURCE_MODE=" .env - - - name: Changing URL for rpc server to localhost - run: sed -i "s/'jsonrpc'/'localhost'/g" .env - - - name: Configure LLM provider - env: - OPENROUTERAPIKEY: ${{ secrets.OPENROUTERAPIKEY }} - LLM_PROVIDER: ${{ vars.LLM_PROVIDER || 'openrouter' }} - LLM_MODEL: ${{ vars.LLM_MODEL || 'deepseek/deepseek-v3.2' }} - run: | - # Inject OpenRouter API key - sed -i "/^OPENROUTERAPIKEY *= *''$/d" .env - printf "\nOPENROUTERAPIKEY='$OPENROUTERAPIKEY'\n" >> .env - - # Override validators config with configured provider/model - sed -i "/^VALIDATORS_CONFIG_JSON=/,/^]'/d" .env - printf "\nVALIDATORS_CONFIG_JSON='[\n {\"stake\": 100, \"provider\": \"$LLM_PROVIDER\", \"model\": \"$LLM_MODEL\", \"amount\": 5}\n]'\n" >> .env - - - name: Extract GenVM version for cache key - id: genvm - run: | - GENVM_REF_VALUE="${GENVM_REF:-}" - if [[ -z "$GENVM_REF_VALUE" && -f .env ]]; then - GENVM_REF_VALUE="$(sed -n 's/^GENVM_REF=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -n "$GENVM_REF_VALUE" ]]; then - GENVM_CACHE_REF="${GENVM_REF_VALUE##*:}" - echo "binding=ref-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - echo "bake_cache=,scope=genvm-$GENVM_CACHE_REF" >> "$GITHUB_OUTPUT" - else - GENVM_TAG_VALUE="${GENVM_TAG:-}" - if [[ -z "$GENVM_TAG_VALUE" && -f .env ]]; then - GENVM_TAG_VALUE="$(sed -n 's/^GENVM_TAG=["'\'']\{0,1\}\([^"'\'']*\)["'\'']\{0,1\}.*/\1/p' .env | head -n1)" - fi - if [[ -z "$GENVM_TAG_VALUE" ]]; then - echo "::warning::GENVM_TAG not set (this job runs prebuilt images) — using shared 'untagged' precompile cache key that will not invalidate across GenVM upgrades" - GENVM_TAG_VALUE="untagged" - fi - echo "binding=$GENVM_TAG_VALUE" >> "$GITHUB_OUTPUT" - echo "bake_cache=" >> "$GITHUB_OUTPUT" - fi - - - name: Restore GenVM precompile cache - uses: actions/cache@v5 - with: - path: ${{ env.GENVM_CACHE_DIR }} - key: genvm-precompile-${{ runner.os }}-${{ runner.arch }}-${{ steps.genvm.outputs.binding }} - - - name: Prepare GenVM cache directory - run: | - sudo mkdir -p "$GENVM_CACHE_DIR/pc" - sudo chown -R 999:999 "$GENVM_CACHE_DIR" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build images with cache (buildx bake) - uses: docker/bake-action@v6 - with: - files: | - ./docker-compose.yml - targets: | - database-migration - jsonrpc - consensus-worker - set: | - *.cache-from=type=gha${{ steps.genvm.outputs.bake_cache }} - *.cache-to=type=gha,mode=max${{ steps.genvm.outputs.bake_cache }} - database-migration.tags=genlayer-studio-database-migration:latest - jsonrpc.tags=genlayer-studio-jsonrpc:latest - consensus-worker.tags=genlayer-studio-consensus-worker:latest - load: true - - - name: Precompile GenVM cache - timeout-minutes: 30 - run: docker/scripts/precompile_genvm.sh - - - name: Run backend stack with multiple workers - timeout-minutes: 5 - run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d --no-build --wait database-migration jsonrpc consensus-worker - env: - CONSENSUS_WORKERS: 3 - - - name: Verify services are ready - run: | - echo "Services passed healthchecks, verifying RPC endpoint..." - curl -sS -X POST -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"ping","params":[],"id":1}' \ - http://localhost:4000/api - - - name: Verify Chain ID - run: | - echo "Verifying chain ID is readable..." - for attempt in 1 2 3 4 5; do - echo "Attempt $attempt/5..." - response=$(curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}') - echo "Response: $response" - - chain_id=$(python3 -c "import json,sys; r=json.loads(sys.argv[1]); print(r.get('result',''))" "$response" 2>/dev/null || true) - if [ -n "$chain_id" ]; then - echo "Chain ID: $chain_id" - break - fi - - if [ "$attempt" -lt 5 ]; then - echo "Retrying in 5s..." - sleep 5 - fi - done - - if [ -z "$chain_id" ]; then - echo "ERROR: Failed to read chain ID after 5 attempts" - exit 1 - fi - - - name: Setup validators - run: | - echo "Creating 5 validators with retry logic..." - created=0 - for i in {1..5}; do - success=false - for attempt in 1 2 3; do - echo "Creating validator $i/5 (attempt $attempt/3)..." - response=$(curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"method\":\"sim_createRandomValidator\",\"params\":[1],\"id\":$i}") - - has_result=$(python3 -c "import json,sys; r=json.loads(sys.argv[1]); print('yes' if 'result' in r else 'no')" "$response" 2>/dev/null || echo "no") - if [ "$has_result" = "yes" ]; then - echo "Validator $i created" - created=$((created + 1)) - success=true - break - fi - - echo "Failed: $response" - if [ "$attempt" -lt 3 ]; then - echo "Retrying in 3s..." - sleep 3 - fi - done - - if [ "$success" = false ]; then - echo "ERROR: Could not create validator $i after 3 attempts" - exit 1 - fi - - # Brief pause between validators - if [ "$i" -lt 5 ]; then sleep 2; fi - done - - echo "Waiting 5 seconds for validators to register..." - sleep 5 - - # Verify validator count with retry - for attempt in 1 2 3; do - response=$(curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"sim_countValidators","params":[],"id":100}') - count=$(python3 -c "import json,sys; r=json.loads(sys.argv[1]); print(r.get('result',0))" "$response" 2>/dev/null || echo "0") - echo "Validator count: $count (attempt $attempt/3)" - - if [ "$count" -ge 5 ]; then - echo "All $count validators registered" - break - fi - - if [ "$attempt" -lt 3 ]; then sleep 3; fi - done - - if [ "$count" -lt 5 ]; then - echo "ERROR: Expected at least 5 validators, found $count" - curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"sim_getAllValidators","params":[],"id":101}' - exit 1 - fi - - - name: Run Load Test - Contract Deploy and Read - run: | - cd tests/load - - # First, let's wait a bit after validator setup - echo "Waiting 15 seconds for system to stabilize after validator setup..." - sleep 15 - - # Install required Python packages - pip install genlayer-py requests - - # Deploy contracts one by one with delays - echo "=== Contract Deployment Test ===" - echo "Deploying 3 contracts with delays..." - - CONTRACT_ADDRESSES=() - DEPLOY_SUCCESS=0 - DEPLOY_FAIL=0 - - for i in {1..3}; do - echo "" - echo "[Deploy $i/3] Starting deployment..." - - # Run the deployment script in a subshell to capture both output and exit code - set +e # Disable exit on error temporarily - result=$(python3 deploy_contract/wizard_deploy.py "http://localhost:4000/api" 2>&1) - exit_code=$? - set -e # Re-enable exit on error - - echo "[Deploy $i/3] Script exit code: $exit_code" - echo "[Deploy $i/3] Script output: $result" - - # Look for a contract address in the output - addr=$(echo "$result" | grep -oE "0x[a-fA-F0-9]{40}" | tail -n 1) - - # Consider deployment successful if we found an address, regardless of exit code - if [ -n "$addr" ]; then - echo "[Deploy $i/3] ✅ Success - Contract: $addr" - CONTRACT_ADDRESSES+=("$addr") - DEPLOY_SUCCESS=$((DEPLOY_SUCCESS + 1)) - - # Wait longer after successful deployment - if [ $i -lt 3 ]; then - echo "Waiting 3 seconds for contract to be fully processed..." - sleep 3 - fi - else - echo "[Deploy $i/3] ❌ Failed - no address returned" - echo "Output: $result" - DEPLOY_FAIL=$((DEPLOY_FAIL + 1)) - - # Still wait between failed attempts - if [ $i -lt 3 ]; then - echo "Waiting 10 seconds before retry..." - sleep 10 - fi - fi - done - - echo "" - echo "=== Deployment Summary ===" - echo "Successful: $DEPLOY_SUCCESS" - echo "Failed: $DEPLOY_FAIL" - - if [ $DEPLOY_SUCCESS -eq 0 ]; then - echo "❌ All deployments failed" - exit 1 - fi - - # Wait for contracts to be indexed - echo "" - echo "Waiting 3 seconds for contracts to be indexed..." - sleep 3 - - # Read from deployed contracts - echo "" - echo "=== Reading Contract States ===" - READ_SUCCESS=0 - READ_FAIL=0 - - for addr in "${CONTRACT_ADDRESSES[@]}"; do - echo "Reading from contract: $addr" - - # Create temporary file with address - echo "$addr" > temp_contract.addr - - # Run read script with proper error handling - set +e # Disable exit on error temporarily - read_output=$(python3 deploy_contract/wizard_read.py temp_contract.addr "http://localhost:4000/api" 2>&1) - read_exit_code=$? - set -e # Re-enable exit on error - - echo "Read exit code: $read_exit_code" - echo "Read output: $read_output" - - # Check if read was successful - if [ $read_exit_code -eq 0 ]; then - echo "✅ Successfully read contract state" - READ_SUCCESS=$((READ_SUCCESS + 1)) - else - echo "❌ Failed to read contract state (exit code: $read_exit_code)" - READ_FAIL=$((READ_FAIL + 1)) - fi - - rm -f temp_contract.addr - - # Add delay between reads to avoid overwhelming the system - echo "Waiting 5 seconds before next read..." - sleep 5 - done - - echo "" - echo "=== Final Summary ===" - echo "Deployments: $DEPLOY_SUCCESS successful, $DEPLOY_FAIL failed" - echo "Reads: $READ_SUCCESS successful, $READ_FAIL failed" - - # More lenient failure conditions - only fail if ALL tests failed - if [ $DEPLOY_SUCCESS -eq 0 ] && [ ${#CONTRACT_ADDRESSES[@]} -eq 0 ]; then - echo "❌ Critical failure: No contracts were deployed successfully" - exit 1 - fi - - # Warn but don't fail if some tests failed - if [ $DEPLOY_FAIL -gt 0 ] || [ $READ_FAIL -gt 0 ]; then - echo "⚠️ Warning: Some tests failed, but continuing as we had at least $DEPLOY_SUCCESS successful deployments" - fi - - - name: Run Load Test - All Read Setup Endpoints - run: | - cd tests/load - - # Wait before starting endpoint tests - echo "Waiting 10 seconds before starting endpoint tests..." - sleep 10 - - # Run with reduced load to avoid overwhelming the system - echo "Running endpoint load tests with reduced load..." - chmod +x load_test_all_read_setup_endpoints.sh - chmod +x test_endpoint.sh - - # Run the load test script with proper error handling and capture output - set +e # Disable exit on error temporarily - - echo "=== Starting Endpoint Load Tests ===" - echo "Configuration: REQUESTS=100 CONCURRENCY=10" - echo "" - - # Reduce the load: 100 requests with 10 concurrent connections - # Capture both stdout and stderr - BASE_URL="http://localhost:4000/api" REQUESTS=100 CONCURRENCY=10 ./load_test_all_read_setup_endpoints.sh 2>&1 | tee endpoint_test_output.log - load_test_exit_code=${PIPESTATUS[0]} - - set -e # Re-enable exit on error - - echo "" - echo "=== Load Test Results ===" - echo "Load test exit code: $load_test_exit_code" - - # Parse test results from the output - if [ -f endpoint_test_output.log ]; then - TOTAL_TESTS=$(grep "Total Tests:" endpoint_test_output.log | awk '{print $3}' || echo "0") - PASSED_TESTS=$(grep "Passed:" endpoint_test_output.log | awk '{print $2}' || echo "0") - FAILED_TESTS=$(grep "Failed:" endpoint_test_output.log | awk '{print $2}' || echo "0") - - echo "Test Summary:" - echo " Total: $TOTAL_TESTS" - echo " Passed: $PASSED_TESTS" - echo " Failed: $FAILED_TESTS" - fi - - # Check the exit code and test results - if [ $load_test_exit_code -eq 0 ]; then - echo "✅ Endpoint load tests completed successfully" - else - echo "⚠️ Some endpoint tests failed (exit code: $load_test_exit_code)" - - # Only consider it a critical failure if more than 50% of tests failed - if [ -n "$TOTAL_TESTS" ] && [ "$TOTAL_TESTS" -gt 0 ] && [ -n "$FAILED_TESTS" ]; then - FAILURE_RATE=$(awk "BEGIN {printf \"%.0f\", ($FAILED_TESTS/$TOTAL_TESTS)*100}") - echo "Failure rate: ${FAILURE_RATE}%" - - if [ "$FAILURE_RATE" -gt 50 ]; then - echo "❌ Critical: More than 50% of tests failed" - - # Check if services are still alive - echo "Checking if services are still responsive..." - sleep 5 - - set +e - curl -s -X POST -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"ping","params":[],"id":1}' \ - http://localhost:4000/api | grep -q "OK" - ping_result=$? - set -e - - if [ $ping_result -eq 0 ]; then - echo "✅ RPC server is still responding" - echo "⚠️ High failure rate but services are alive - likely load-related issues" - else - echo "❌ RPC server not responding - services may have crashed" - # Show docker logs for debugging - echo "Docker compose logs (last 50 lines):" - docker compose logs --tail=50 - exit 1 - fi - else - echo "⚠️ Acceptable failure rate (${FAILURE_RATE}%) - continuing" - fi - fi - - echo "⚠️ Continuing despite some endpoint test failures (load testing is best-effort)" - fi - - # Check if the report files were generated - if [ -f api_test_report.json ]; then - echo "✅ JSON report generated: api_test_report.json" - fi - if [ -f api_test_report.html ]; then - echo "✅ HTML report generated: api_test_report.html" - fi - - - name: Re-setup validators for state integrity test - run: | - echo "Re-creating validators (previous step cleaned them up)..." - for i in {1..5}; do - success=false - for attempt in 1 2 3; do - echo "Creating validator $i/5 (attempt $attempt/3)..." - response=$(curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d "{\"jsonrpc\":\"2.0\",\"method\":\"sim_createRandomValidator\",\"params\":[1],\"id\":$i}") - - has_result=$(python3 -c "import json,sys; r=json.loads(sys.argv[1]); print('yes' if 'result' in r else 'no')" "$response" 2>/dev/null || echo "no") - if [ "$has_result" = "yes" ]; then - echo "Validator $i created" - success=true - break - fi - - echo "Failed: $response" - if [ "$attempt" -lt 3 ]; then sleep 3; fi - done - - if [ "$success" = false ]; then - echo "ERROR: Could not create validator $i after 3 attempts" - exit 1 - fi - - if [ "$i" -lt 5 ]; then sleep 2; fi - done - - echo "Waiting 5 seconds for validators to register..." - sleep 5 - - # Verify validators exist - response=$(curl -s -X POST http://localhost:4000/api \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"sim_countValidators","params":[],"id":100}') - count=$(python3 -c "import json,sys; r=json.loads(sys.argv[1]); print(r.get('result',0))" "$response" 2>/dev/null || echo "0") - echo "Validator count: $count" - - if [ "$count" -lt 5 ]; then - echo "ERROR: Expected at least 5 validators, found $count" - exit 1 - fi - - - name: Run State Integrity Test (lost update detection) - run: | - cd tests/load - pip install genlayer-py requests - python3 test_state_integrity.py http://localhost:4000/api --txs 20 - - - name: Dump Docker Compose logs - run: docker compose logs - if: failure() - - - name: Shutdown Docker Compose - if: always() - run: docker compose down -v diff --git a/.github/workflows/manual-docker-release.yml b/.github/workflows/manual-docker-release.yml deleted file mode 100644 index 29a643e4d..000000000 --- a/.github/workflows/manual-docker-release.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Create Release Tag and Release - -on: - workflow_dispatch: - inputs: - branch: - description: 'Version branch to tag (e.g., v0.121)' - required: true - type: string - version: - description: 'Version tag (e.g., v0.81.0)' - required: true - type: string - -permissions: - contents: write - -jobs: - create-release-tag: - name: Create Release Tag - runs-on: ubuntu-latest - steps: - - name: Validate version format - env: - RELEASE_BRANCH: ${{ github.event.inputs.branch }} - RELEASE_VERSION: ${{ github.event.inputs.version }} - run: | - set -euo pipefail - if [[ ! "$RELEASE_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9-]+)?$ ]]; then - echo "Error: Version must be in format v*.*.*[-suffix] (e.g., v0.81.0 or v0.81.0-beta)" - exit 1 - fi - expected_branch="$(sed -E 's/^((v[0-9]+)\.([0-9]+))\..*$/\1/' <<< "$RELEASE_VERSION")" - if [[ "$RELEASE_BRANCH" != "$expected_branch" ]]; then - echo "Error: version $RELEASE_VERSION must be created from branch ${expected_branch}" - exit 1 - fi - - - name: Checkout specified branch - uses: actions/checkout@v6 - with: - ref: ${{ github.event.inputs.branch }} - fetch-depth: 0 - - - name: Create and push tag - env: - RELEASE_BRANCH: ${{ github.event.inputs.branch }} - RELEASE_VERSION: ${{ github.event.inputs.version }} - run: | - set -euo pipefail - git config user.name "GitHub Actions" - git config user.email "actions@github.com" - git fetch --force origin "refs/tags/${RELEASE_VERSION}:refs/tags/${RELEASE_VERSION}" || true - - head_commit="$(git rev-parse HEAD)" - if git rev-parse --verify --quiet "refs/tags/${RELEASE_VERSION}" >/dev/null; then - tag_commit="$(git rev-parse "${RELEASE_VERSION}^{commit}")" - if [[ "$tag_commit" != "$head_commit" ]]; then - echo "Error: tag $RELEASE_VERSION points at $tag_commit, expected $head_commit" >&2 - exit 1 - fi - echo "Tag $RELEASE_VERSION already exists at branch head; continuing release." - else - git tag -a "$RELEASE_VERSION" -m "Release $RELEASE_VERSION from branch $RELEASE_BRANCH" - git push origin "refs/tags/${RELEASE_VERSION}" - fi - - release-from-tag: - name: Build Images and Trigger Release Lane - needs: create-release-tag - permissions: - contents: read - uses: ./.github/workflows/release-from-tag.yml - with: - tag: ${{ github.event.inputs.version }} - secrets: - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - RELEASE_LANE_APP_KEY: ${{ secrets.RELEASE_LANE_APP_KEY }} diff --git a/.github/workflows/release-from-tag.yml b/.github/workflows/release-from-tag.yml deleted file mode 100644 index f7365d7b5..000000000 --- a/.github/workflows/release-from-tag.yml +++ /dev/null @@ -1,173 +0,0 @@ -name: Release from version tag - -on: - push: - tags: - - "v*.*.*" - workflow_dispatch: - inputs: - tag: - description: "Release tag to build and promote" - required: true - type: string - workflow_call: - inputs: - tag: - description: "Release tag to build and promote" - required: true - type: string - secrets: - DOCKERHUB_TOKEN: - required: true - RELEASE_LANE_APP_KEY: - required: true - -permissions: - contents: read - -concurrency: - group: release-${{ inputs.tag || github.ref_name }} - cancel-in-progress: false - -jobs: - validate-tag: - name: Validate release tag - runs-on: ubuntu-latest - permissions: - contents: read - outputs: - tag: ${{ steps.validate.outputs.tag }} - commit: ${{ steps.validate.outputs.commit }} - version_branch: ${{ steps.validate.outputs.version_branch }} - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Validate tag belongs to matching version branch - id: validate - env: - INPUT_TAG: ${{ inputs.tag || '' }} - run: | - set -euo pipefail - - tag="${INPUT_TAG:-${GITHUB_REF_NAME}}" - if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9-]+)?$ ]]; then - echo "Invalid release tag: $tag" >&2 - exit 1 - fi - - version_branch="$(sed -E 's/^((v[0-9]+)\.([0-9]+))\..*$/\1/' <<< "$tag")" - - git fetch --force origin \ - "refs/heads/${version_branch}:refs/remotes/origin/${version_branch}" \ - "refs/tags/${tag}:refs/tags/${tag}" - - tag_commit="$(git rev-parse "${tag}^{commit}")" - branch_head="$(git rev-parse "refs/remotes/origin/${version_branch}")" - - if [[ "$tag_commit" != "$branch_head" ]]; then - echo "Tag $tag ($tag_commit) must point at the current origin/${version_branch} head ($branch_head)" >&2 - exit 1 - fi - - echo "Release tag: $tag" - echo "Version branch: origin/${version_branch}" - echo "Tag commit: $tag_commit" - echo "Version branch head: $branch_head" - - { - echo "tag=$tag" - echo "commit=$tag_commit" - echo "version_branch=$version_branch" - } >> "$GITHUB_OUTPUT" - - push-docker-images: - name: Build images and push to DockerHub - needs: validate-tag - permissions: - contents: read - uses: ./.github/workflows/docker-build-and-push-all.yml - with: - ref: ${{ needs.validate-tag.outputs.tag }} - image_tag: ${{ needs.validate-tag.outputs.tag }} - secrets: - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - - trigger-workload-release-lane: - name: Trigger AWS Release Lane - runs-on: ubuntu-latest - permissions: - contents: read - needs: - - validate-tag - - push-docker-images - steps: - - name: Get release-lane token - id: release_lane_token - uses: actions/create-github-app-token@v3 - with: - client-id: ${{ vars.RELEASE_LANE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_LANE_APP_KEY }} - owner: ${{ github.repository_owner }} - repositories: devexp-argocd-apps - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - - - name: Resolve image digests - id: digests - env: - RELEASE_TAG: ${{ needs.validate-tag.outputs.tag }} - run: | - set -euo pipefail - - resolve_digest() { - local image_ref="$1" - local digest="" - for attempt in {1..20}; do - digest="$(docker buildx imagetools inspect "$image_ref" --format '{{json .Manifest.Digest}}' 2>/dev/null | tr -d '\"' || true)" - if [[ "$digest" == sha256:* ]]; then - echo "$digest" - return 0 - fi - echo "Digest not available yet for ${image_ref}; attempt ${attempt}/20" >&2 - sleep 15 - done - echo "Failed to resolve digest for ${image_ref}" >&2 - return 1 - } - - jsonrpc_digest="$(resolve_digest "yeagerai/simulator-jsonrpc:${RELEASE_TAG}")" - worker_digest="$(resolve_digest "yeagerai/simulator-consensus-worker:${RELEASE_TAG}")" - frontend_digest="$(resolve_digest "yeagerai/simulator-frontend:${RELEASE_TAG}")" - explorer_digest="$(resolve_digest "yeagerai/simulator-explorer:${RELEASE_TAG}")" - migration_digest="$(resolve_digest "yeagerai/simulator-database-migration:${RELEASE_TAG}")" - - { - echo "jsonrpc=yeagerai/simulator-jsonrpc@${jsonrpc_digest}" - echo "worker=yeagerai/simulator-consensus-worker@${worker_digest}" - echo "frontend=yeagerai/simulator-frontend@${frontend_digest}" - echo "explorer=yeagerai/simulator-explorer@${explorer_digest}" - echo "migration=yeagerai/simulator-database-migration@${migration_digest}" - } >> "$GITHUB_OUTPUT" - - - name: Trigger AWS release lane (devexp-argocd-apps) - env: - GH_TOKEN: ${{ steps.release_lane_token.outputs.token }} - JSONRPC: ${{ steps.digests.outputs.jsonrpc }} - WORKER: ${{ steps.digests.outputs.worker }} - FRONTEND: ${{ steps.digests.outputs.frontend }} - EXPLORER: ${{ steps.digests.outputs.explorer }} - MIGRATION: ${{ steps.digests.outputs.migration }} - run: | - set -euo pipefail - gh api repos/genlayerlabs/devexp-argocd-apps/dispatches \ - --method POST \ - -f event_type=release-backend \ - -F "client_payload[image]=$JSONRPC" \ - -F "client_payload[consensus_worker_image]=$WORKER" \ - -F "client_payload[frontend_image]=$FRONTEND" \ - -F "client_payload[explorer_image]=$EXPLORER" \ - -F "client_payload[migration_image]=$MIGRATION" diff --git a/.github/workflows/retarget-main-prs.yaml b/.github/workflows/retarget-main-prs.yaml deleted file mode 100644 index 1a3a31667..000000000 --- a/.github/workflows/retarget-main-prs.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Retarget main PRs - -# main is a static/default alias of the active dev branch. Contributions should -# target the active dev branch directly; PRs opened against main are retargeted -# automatically so required checks and release-train rules run in the right -# branch context. -# -# pull_request_target is used for the write-scoped token. This workflow never -# checks out or executes PR head code; it reads only trusted base-branch files. - -on: - pull_request_target: - types: [opened, reopened, synchronize, edited, ready_for_review] - -permissions: - contents: read - pull-requests: write - issues: write - -defaults: - run: - shell: bash - -jobs: - retarget: - if: github.event.pull_request.base.ref == 'main' - runs-on: ubuntu-latest - steps: - - name: Retarget PR to active dev branch - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - BASE_REF: ${{ github.event.pull_request.base.ref }} - run: | - set -euo pipefail - - active_branch="$( - gh api "/repos/${GITHUB_REPOSITORY}/contents/support/ci/ACTIVE_DEV_BRANCH?ref=${BASE_REF}" \ - --jq '.content' | base64 -d | tr -d '[:space:]' - )" - if [[ -z "${active_branch}" || "${active_branch}" == "main" ]]; then - echo "::error::support/ci/ACTIVE_DEV_BRANCH must name a non-main dev branch" - exit 1 - fi - - gh pr edit "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --base "${active_branch}" - - gh pr comment "${PR_NUMBER}" --repo "${GITHUB_REPOSITORY}" --body "$(cat < semgrep.sarif - - - name: Upload SARIF file for GitHub Advanced Security Dashboard - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: semgrep.sarif - if: always() diff --git a/.github/workflows/unit-tests-pr.yml b/.github/workflows/unit-tests-pr.yml deleted file mode 100644 index e5fc72414..000000000 --- a/.github/workflows/unit-tests-pr.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: "Unit Tests" - -on: - pull_request: - types: - - opened - - synchronize - push: - branches: - - main # so that test reports get uploaded to Codecov and SonarCloud - workflow_dispatch: - -permissions: - contents: read - -jobs: - frontend-unit-tests: - if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]') - name: Unit Tests - uses: ./.github/workflows/frontend-unit-tests.yml - secrets: - codecov_token: ${{ secrets.CODECOV_TOKEN }} - - explorer-unit-tests: - if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]') - name: Explorer Unit Tests - runs-on: ubuntu-latest - defaults: - run: - working-directory: explorer - steps: - - uses: actions/checkout@v6 - - name: Use Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - - run: npm ci --ignore-scripts - - run: npm run test:fee-accounting - - run: npm run lint - - run: npm run build - - backend-lint: - if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]') - name: Backend lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - run: pip install ruff==0.15.22 - - run: ruff check - - backend-unit-tests: - if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.12" - cache: pip - - run: pip install -r backend/requirements.txt - - run: pip install pytest-cov pytest-xdist - - run: pytest tests/unit --cov=backend --cov-report=xml --cov-branch -n auto - - # Backend coverage.xml previously had exactly one consumer (SonarCloud). - # Codecov measures the actual PR diff rather than a stale branch baseline. - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5.5.2 - with: - verbose: true - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: false - files: coverage.xml - flags: backend