From 0f95277ddf2a7c72f29d185281f4277859747e08 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Tue, 16 Jul 2024 08:23:33 -0700 Subject: [PATCH] refactor(ci): finish matrix migration and update release workflow (#2475) * refactor(ci): finish matrix migration and update release workflow * update readme go version * use actions script * test? * use core * use context references --- .github/workflows/build.yml | 2 +- .github/workflows/e2e.yml | 122 +++++-- .github/workflows/execute_advanced_tests.yaml | 119 ------- .github/workflows/publish-release.yml | 329 +----------------- .github/workflows/reusable-e2e.yml | 2 +- .github/workflows/sast-linters.yml | 4 +- readme.md | 2 +- 7 files changed, 102 insertions(+), 478 deletions(-) delete mode 100644 .github/workflows/execute_advanced_tests.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea1e2cad2d..c8b5869c15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - release/* merge_group: pull_request: branches: @@ -12,7 +13,6 @@ on: - synchronize - opened - reopened - - ready_for_review concurrency: group: pr-testing-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e0dc06924a..0213c3fdfc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -4,15 +4,45 @@ on: push: branches: - develop + - release/* pull_request: branches: - "*" merge_group: - workflow_dispatch: schedule: # run at 6AM UTC Daily # 6AM UTC -> 11PM PT - cron: "0 6 * * *" + workflow_dispatch: + inputs: + default-test: + type: boolean + required: false + default: false + upgrade-light-test: + type: boolean + required: false + default: false + upgrade-test: + type: boolean + required: false + default: false + admin-test: + type: boolean + required: false + default: false + upgrade-import-mainnet-test: + type: boolean + required: false + default: false + performance-test: + type: boolean + required: false + default: false + stateful-data-test: + type: boolean + required: false + default: false concurrency: group: e2e-${{ github.head_ref || github.sha }} @@ -27,34 +57,61 @@ jobs: DEFAULT_TESTS: ${{ steps.matrix-conditionals.outputs.DEFAULT_TESTS }} UPGRADE_TESTS: ${{ steps.matrix-conditionals.outputs.UPGRADE_TESTS }} UPGRADE_LIGHT_TESTS: ${{ steps.matrix-conditionals.outputs.UPGRADE_LIGHT_TESTS }} + UPGRADE_IMPORT_MAINNET_TESTS: ${{ steps.matrix-conditionals.outputs.UPGRADE_IMPORT_MAINNET_TESTS }} ADMIN_TESTS: ${{ steps.matrix-conditionals.outputs.ADMIN_TESTS }} + PERFORMANCE_TESTS: ${{ steps.matrix-conditionals.outputs.PERFORMANCE_TESTS }} + STATEFUL_DATA_TESTS: ${{ steps.matrix-conditionals.outputs.STATEFUL_DATA_TESTS }} steps: - # use cli rather than event context to avoid race conditions (label added after push) + # use api rather than event context to avoid race conditions (label added after push) - id: matrix-conditionals - run: | - if [[ ${{ github.event_name }} == 'pull_request' ]]; then - echo "DEFAULT_TESTS=true" >> $GITHUB_OUTPUT - labels=$(gh pr view -R ${{github.repository}} ${{github.event.pull_request.number}} --json labels -q '.labels[].name') - if [[ "$labels" == *"UPGRADE_TESTS"* ]]; then - echo "UPGRADE_TESTS=true" >> $GITHUB_OUTPUT - fi - - if [[ "$labels" == *"UPGRADE_LIGHT_TESTS"* ]]; then - echo "UPGRADE_LIGHT_TESTS=true" >> $GITHUB_OUTPUT - fi - - if [[ "$labels" == *"ADMIN_TESTS"* ]]; then - echo "ADMIN_TESTS=true" >> $GITHUB_OUTPUT - fi - elif [[ ${{ github.event_name }} == 'merge_group' ]]; then - echo "DEFAULT_TESTS=true" >> $GITHUB_OUTPUT - elif [[ ${{ github.event_name }} == 'push' && ${{ github.ref }} == 'refs/heads/develop' ]]; then - echo "DEFAULT_TESTS=true" >> $GITHUB_OUTPUT - elif [[ ${{ github.event_name }} == 'schedule' ]]; then - echo "UPGRADE_TESTS=true" >> $GITHUB_OUTPUT - echo "UPGRADE_LIGHT_TESTS=true" >> $GITHUB_OUTPUT - echo "ADMIN_TESTS=true" >> $GITHUB_OUTPUT - fi + uses: actions/github-script@v7 + with: + script: | + console.log(context); + if (context.eventName === 'pull_request') { + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + const labels = pr.labels.map(label => label.name); + console.log("labels:", labels); + core.setOutput('DEFAULT_TESTS', true); + core.setOutput('UPGRADE_TESTS', labels.includes('UPGRADE_TESTS')); + core.setOutput('UPGRADE_LIGHT_TESTS', labels.includes('UPGRADE_LIGHT_TESTS')); + core.setOutput('UPGRADE_IMPORT_MAINNET_TESTS', labels.includes('UPGRADE_IMPORT_MAINNET_TESTS')); + core.setOutput('ADMIN_TESTS', labels.includes('ADMIN_TESTS')); + core.setOutput('PERFORMANCE_TESTS', labels.includes('PERFORMANCE_TESTS')); + core.setOutput('STATEFUL_DATA_TESTS', labels.includes('STATEFUL_DATA_TESTS')); + } else if (context.eventName === 'merge_group') { + core.setOutput('DEFAULT_TESTS', true); + } else if (context.eventName === 'push' && context.ref === 'refs/heads/develop') { + core.setOutput('DEFAULT_TESTS', true); + } else if (context.eventName === 'push' && context.ref.startsWith('refs/heads/release/')) { + core.setOutput('DEFAULT_TESTS', true); + core.setOutput('UPGRADE_TESTS', true); + core.setOutput('UPGRADE_LIGHT_TESTS', true); + core.setOutput('UPGRADE_IMPORT_MAINNET_TESTS', true); + core.setOutput('ADMIN_TESTS', true); + core.setOutput('PERFORMANCE_TESTS', true); + core.setOutput('STATEFUL_DATA_TESTS', true); + } else if (context.eventName === 'schedule') { + core.setOutput('DEFAULT_TESTS', true); + core.setOutput('UPGRADE_TESTS', true); + core.setOutput('UPGRADE_LIGHT_TESTS', true); + core.setOutput('UPGRADE_IMPORT_MAINNET_TESTS', true); + core.setOutput('ADMIN_TESTS', true); + core.setOutput('PERFORMANCE_TESTS', true); + core.setOutput('STATEFUL_DATA_TESTS', true); + } else if (context.eventName === 'workflow_dispatch') { + core.setOutput('DEFAULT_TESTS', context.payload.inputs['default-test']); + core.setOutput('UPGRADE_TESTS', context.payload.inputs['upgrade-test']); + core.setOutput('UPGRADE_LIGHT_TESTS', context.payload.inputs['upgrade-light-test']); + core.setOutput('UPGRADE_IMPORT_MAINNET_TESTS', context.payload.inputs['upgrade-import-mainnet-test']); + core.setOutput('ADMIN_TESTS', context.payload.inputs['admin-test']); + core.setOutput('PERFORMANCE_TESTS', context.payload.inputs['performance-test']); + core.setOutput('STATEFUL_DATA_TESTS', context.payload.inputs['stateful-data-test']); + } e2e: needs: matrix-conditionals @@ -71,9 +128,18 @@ jobs: - make-target: "start-upgrade-test-light" runs-on: ubuntu-20.04 run: ${{ needs.matrix-conditionals.outputs.UPGRADE_LIGHT_TESTS == 'true' }} + - make-target: "start-upgrade-import-mainnet-test" + runs-on: buildjet-16vcpu-ubuntu-2204 + run: ${{ needs.matrix-conditionals.outputs.UPGRADE_IMPORT_MAINNET_TESTS == 'true' }} - make-target: "start-e2e-admin-test" runs-on: ubuntu-20.04 run: ${{ needs.matrix-conditionals.outputs.ADMIN_TESTS == 'true' }} + - make-target: "start-e2e-performance-test" + runs-on: buildjet-4vcpu-ubuntu-2204 + run: ${{ needs.matrix-conditionals.outputs.PERFORMANCE_TESTS == 'true' }} + - make-target: "start-e2e-import-mainnet-test" + runs-on: buildjet-16vcpu-ubuntu-2204 + run: ${{ needs.matrix-conditionals.outputs.STATEFUL_DATA_TESTS == 'true' }} name: ${{ matrix.make-target }} uses: ./.github/workflows/reusable-e2e.yml with: @@ -84,7 +150,9 @@ jobs: # this allows you to set a required status check e2e-ok: runs-on: ubuntu-22.04 - needs: e2e + needs: + - matrix-conditionals + - e2e if: always() steps: - run: | diff --git a/.github/workflows/execute_advanced_tests.yaml b/.github/workflows/execute_advanced_tests.yaml deleted file mode 100644 index e8a4683812..0000000000 --- a/.github/workflows/execute_advanced_tests.yaml +++ /dev/null @@ -1,119 +0,0 @@ -name: "TESTING:ADVANCED:E2E" - -on: - workflow_dispatch: - inputs: - e2e-stateful-upgrade-test: - type: boolean - required: false - default: false - e2e-performance-test: - type: boolean - required: false - default: false - e2e-stateful-data-test: - type: boolean - required: false - default: false - debug: - type: boolean - required: false - default: false - schedule: - # run at 6AM UTC Daily - # 6AM UTC -> 11PM PT - - cron: "0 6 * * *" - -jobs: - e2e-stateful-upgrade-test: - if: ${{ github.event.inputs.e2e-stateful-upgrade-test == 'true' || github.event_name == 'schedule' }} - runs-on: buildjet-16vcpu-ubuntu-2204 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - uses: actions/checkout@v4 - - - name: Start Test - run: make start-upgrade-import-mainnet-test - - - name: Watch Test - run: | - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & - exit $(docker wait "${container_id}") - - - name: Full Log Dump On Failure - if: failure() - run: | - make stop-localnet - - - name: Notify Slack on Failure - if: failure() && github.event_name == 'schedule' - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,commit,author,action,eventName,ref,workflow,job,took - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} - - e2e-performance-test: - if: ${{ github.event.inputs.e2e-performance-test == 'true' }} - runs-on: buildjet-4vcpu-ubuntu-2204 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - uses: actions/checkout@v4 - - - name: Start Test - run: make start-e2e-performance-test - - - name: Watch Test - run: | - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & - exit $(docker wait "${container_id}") - - - name: Full Log Dump On Failure - if: failure() - run: | - make stop-localnet - - - name: Notify Slack on Failure - if: failure() && github.event_name == 'schedule' - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,commit,author,action,eventName,ref,workflow,job,took - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} - - e2e-stateful-data-test: - if: ${{ github.event.inputs.e2e-stateful-data-test == 'true' || github.event_name == 'schedule' }} - runs-on: buildjet-16vcpu-ubuntu-2204 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - uses: actions/checkout@v4 - - - name: Start Test - run: make start-e2e-import-mainnet-test - - - name: Watch Test - run: | - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & - exit $(docker wait "${container_id}") - - - name: Full Log Dump On Failure - if: failure() - run: | - make stop-localnet - - - name: Notify Slack on Failure - if: failure() && github.event_name == 'schedule' - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,commit,author,action,eventName,ref,workflow,job,took - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_CI_ALERTS }} \ No newline at end of file diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 44e3fa0538..ff454a4032 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -11,7 +11,7 @@ on: type: boolean required: false default: false - description: 'Use this to skip: gosec, gosec-cosmos, check-changelog, check-upgrade-uandler-updated, build-test, smoke-test and go straight to approval step.' + description: 'Use this to skip: check-changelog and check-upgrade-handler-updated go straight to approval step.' skip_release: type: boolean required: false @@ -31,99 +31,6 @@ jobs: run: | echo "${{ github.ref }}" - gosec: - needs: - - check_branch - runs-on: ubuntu-22.04 - env: - GO111MODULE: on - steps: - - name: Checkout Source - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - - name: Run Gosec Security Scanner - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: securego/gosec@v2.19.0 - with: - args: ./... - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - gosec-cosmos: - needs: - - check_branch - runs-on: ubuntu-22.04 - env: - GO111MODULE: on - steps: - - name: Checkout Source - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - - name: Run Cosmos Gosec Security Scanner - if: ${{ github.event.inputs.skip_checks != 'true' }} - run: make lint-cosmos-gosec - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - lint: - needs: - - check_branch - runs-on: ubuntu-22.04 - timeout-minutes: 15 - env: - GO111MODULE: on - steps: - - name: Checkout Source - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - - name: Run golangci-lint - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: golangci/golangci-lint-action@v6 - with: - version: v1.59 - skip-cache: true - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - check-changelog: needs: - check_branch @@ -198,232 +105,6 @@ jobs: run: | echo "continue" - build-test: - needs: - - check_branch - runs-on: ubuntu-22.04 - timeout-minutes: 15 - concurrency: - group: "build-test" - steps: - - name: "Checkout Code" - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - - - name: Set CPU Architecture - if: ${{ github.event.inputs.skip_checks != 'true' }} - shell: bash - run: | - if [ "$(uname -m)" == "aarch64" ]; then - echo "CPU_ARCH=arm64" >> $GITHUB_ENV - elif [ "$(uname -m)" == "x86_64" ]; then - echo "CPU_ARCH=amd64" >> $GITHUB_ENV - else - echo "Unsupported architecture" >&2 - exit 1 - fi - - - name: Install Pipeline Dependencies - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: ./.github/actions/install-dependencies - timeout-minutes: 8 - with: - cpu_architecture: ${{ env.CPU_ARCH }} - skip_python: "true" - skip_aws_cli: "true" - skip_docker_compose: "false" - - - name: Test - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: nick-fields/retry@v2 - with: - timeout_minutes: 20 - max_attempts: 2 - retry_on: error - command: | - echo "Running Build Tests" - make clean - make test-coverage - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - file: coverage.out - token: ${{ secrets.CODECOV_TOKEN }} - slug: zeta-chain/node - - - name: Build zetacored and zetaclientd - if: ${{ github.event.inputs.skip_checks != 'true' }} - env: - CGO_ENABLED: 1 - GOOS: linux - GOARCH: ${{ env.CPU_ARCH }} - run: | - make install - cp "$HOME"/go/bin/* ./ - chmod a+x ./zetacored - ./zetacored version - - - name: Clean Up Workspace - if: always() - shell: bash - run: rm -rf * - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - smoke-test: - needs: - - check_branch - runs-on: ubuntu-22.04 - timeout-minutes: 25 - steps: - - name: "Checkout Code" - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - - - name: Set CPU Architecture - if: ${{ github.event.inputs.skip_checks != 'true' }} - shell: bash - run: | - if [ "$(uname -m)" == "aarch64" ]; then - echo "CPU_ARCH=arm64" >> $GITHUB_ENV - elif [ "$(uname -m)" == "x86_64" ]; then - echo "CPU_ARCH=amd64" >> $GITHUB_ENV - else - echo "Unsupported architecture" >&2 - exit 1 - fi - - - name: Install Pipeline Dependencies - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: ./.github/actions/install-dependencies - timeout-minutes: 8 - with: - cpu_architecture: ${{ env.CPU_ARCH }} - skip_python: "false" - skip_aws_cli: "true" - skip_docker_compose: "false" - - - name: Login to Docker Hub - uses: docker/login-action@v2 - if: ${{ github.event.repository.full_name == 'zetachain-chain/node' && github.event.inputs.skip_checks != 'true' }} - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_READ_ONLY }} - - - name: Build zetanode - if: ${{ github.event.inputs.skip_checks != 'true' }} - run: | - make zetanode - - - name: Start Private Network - if: ${{ github.event.inputs.skip_checks != 'true' }} - run: | - cd contrib/localnet/ - docker compose up -d zetacore0 zetacore1 zetaclient0 zetaclient1 eth bitcoin - - - name: Run Smoke Test - if: ${{ github.event.inputs.skip_checks != 'true' }} - run: | - cd contrib/localnet - docker-compose up orchestrator --exit-code-from orchestrator - if [ $? -ne 0 ]; then - echo "Smoke Test Failed" - exit 1 - fi - - - name: Stop Private Network - if: ${{ always() && github.event.inputs.skip_checks != 'true' }} - run: | - make stop-localnet - - - name: Clean Up Workspace - if: always() - shell: bash - run: sudo rm -rf * - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - e2e-admin-tests: - needs: - - check_branch - runs-on: ubuntu-22.04 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - - - name: Execute e2e-admin-tests - if: ${{ github.event.inputs.skip_checks != 'true' }} - shell: bash - run: | - make start-e2e-admin-test - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & exit $(docker wait "${container_id}") - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - e2e-upgrade-test: - needs: - - check_branch - runs-on: buildjet-16vcpu-ubuntu-2204 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v4 - - - name: Execute upgrade-test - if: ${{ github.event.inputs.skip_checks != 'true' }} - shell: bash - run: | - make start-upgrade-import-mainnet-test - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & exit $(docker wait "${container_id}") - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - - e2e-stateful-data-test: - needs: - - check_branch - runs-on: buildjet-16vcpu-ubuntu-2204 - timeout-minutes: 120 - steps: - - name: "Checkout Code" - if: ${{ github.event.inputs.skip_checks != 'true' }} - uses: actions/checkout@v3 - - - name: Execute stateful-data-test - if: ${{ github.event.inputs.skip_checks != 'true' }} - shell: bash - run: | - make start-e2e-import-mainnet-test - container_id=$(docker ps --filter "ancestor=orchestrator:latest" --format "{{.ID}}") - docker logs -f "${container_id}" & exit $(docker wait "${container_id}") - - - name: Mark Job Complete Skipped - if: ${{ github.event.inputs.skip_checks == 'true' }} - shell: bash - run: | - echo "continue" - publish-release: permissions: id-token: write @@ -431,16 +112,8 @@ jobs: attestations: write if: ${{ github.event.inputs.skip_release == 'false' }} needs: - - gosec - - gosec-cosmos - - lint - check-changelog - check-upgrade-handler-updated - - smoke-test - - build-test - - e2e-admin-tests - - e2e-stateful-data-test - - e2e-upgrade-test - check_branch runs-on: ubuntu-22.04 timeout-minutes: 60 diff --git a/.github/workflows/reusable-e2e.yml b/.github/workflows/reusable-e2e.yml index 2bc75efcfb..d67f53db0e 100644 --- a/.github/workflows/reusable-e2e.yml +++ b/.github/workflows/reusable-e2e.yml @@ -101,7 +101,7 @@ jobs: path: /tmp/logs.txt - name: Notify Slack on Failure - if: failure() && ((github.event_name == 'push' && github.ref == 'refs/heads/develop') || github.event_name == 'schedule') + if: failure() && (github.event_name == 'push' || github.event_name == 'schedule') uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} diff --git a/.github/workflows/sast-linters.yml b/.github/workflows/sast-linters.yml index 30a8ab1f33..270e9cffc6 100644 --- a/.github/workflows/sast-linters.yml +++ b/.github/workflows/sast-linters.yml @@ -1,13 +1,15 @@ name: Linters and SAST on: push: + branches: + - develop + - release/* tags: - "*" merge_group: pull_request: types: - opened - - edited - synchronize concurrency: diff --git a/readme.md b/readme.md index 3bc4c33d1a..3f00b4b7f5 100644 --- a/readme.md +++ b/readme.md @@ -5,7 +5,7 @@ smart contracts and messaging between any blockchain. ## Prerequisites -- [Go](https://golang.org/doc/install) 1.20 +- [Go](https://golang.org/doc/install) 1.22 - [Docker](https://docs.docker.com/install/) and [Docker Compose](https://docs.docker.com/compose/install/) (optional, for running tests locally)