chore: deploy DOScan 11.2.8 #152
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Config | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "docker-compose/docker-compose-mainnet.yml" | |
| - "docker-compose/docker-compose-testnet.yml" | |
| - "docker-compose/docker-compose-gcp-edge.yml" | |
| - "docker-compose/docker-compose-gcp-edge.override.yml" | |
| - "docker-compose/Caddyfile-gcp-mainnet" | |
| - "docker-compose/Caddyfile-gcp-testnet" | |
| - "docker-compose/Caddyfile-gcp-edge" | |
| - "docker-compose/bens/**" | |
| - "docker-compose/configs/interchain-indexer-*.json" | |
| - "docker-compose/envs/common-blockscout.env" | |
| - "docker-compose/envs/common-blockscout-mainnet.env" | |
| - "docker-compose/envs/common-blockscout-testnet.env" | |
| - "docker-compose/envs/common-frontend.env" | |
| - "docker-compose/envs/common-frontend-scan.env" | |
| - "docker-compose/envs/common-frontend-testnet.env" | |
| - "docker-compose/envs/common-smart-contract-verifier.env" | |
| - "docker-compose/envs/common-visualizer.env" | |
| - "scripts/validate-blockscout-env-parity.py" | |
| - "scripts/render-testnet-bens.py" | |
| - "scripts/render-mainnet-bens.py" | |
| - "scripts/validate-testnet-bens.py" | |
| - ".github/scripts/extract-aa-verification-inputs.mjs" | |
| - ".github/scripts/extract-mainnet-aa-verification-inputs.mjs" | |
| - ".github/scripts/mainnet-aa-solc/**" | |
| - ".github/scripts/mainnet-bens-runtime.sh" | |
| - ".github/scripts/mainnet-bens-ui.spec.mjs" | |
| - ".github/scripts/mainnet-aa-source-ui.spec.mjs" | |
| - ".github/scripts/prepare-mainnet-aa-verification.sh" | |
| - ".github/scripts/verify-mainnet-aa-bytecode.mjs" | |
| - ".github/scripts/verify-mainnet-aa-sources.sh" | |
| - ".github/scripts/verify-testnet-aa-sources.sh" | |
| - ".github/workflows/deploy-config.yml" | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: Deploy target | |
| required: true | |
| type: choice | |
| options: | |
| - mainnet | |
| - testnet | |
| - beta | |
| permissions: | |
| contents: read | |
| id-token: write | |
| concurrency: | |
| group: deploy-config-${{ inputs.environment || 'mainnet' }} | |
| cancel-in-progress: false | |
| env: | |
| GCP_PROJECT_ID: dos--ai | |
| GCP_ZONE: asia-southeast1-b | |
| GCP_INSTANCE: doscan-mainnet | |
| GCP_SERVICE_ACCOUNT: doscan-deploy@dos--ai.iam.gserviceaccount.com | |
| GCP_WORKLOAD_IDENTITY_PROVIDER: projects/535583621422/locations/global/workloadIdentityPools/github/providers/doscan-provider | |
| GCP_TESTNET_ZONE: asia-southeast1-a | |
| GCP_TESTNET_INSTANCE: dos-testnet-r0 | |
| DOS_NAMES_REPOSITORY: https://github.com/DOS/DOS-Names-Contracts.git | |
| DOS_NAMES_MAINNET_SUBGRAPH_REF: 6224395661280a739c20ebd8a420913a0dd7fd6e | |
| DOS_NAMES_TESTNET_SUBGRAPH_REF: 130d42ae22881896cab89e33c3c3c096b9b8e989 | |
| jobs: | |
| changes: | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mainnet: ${{ steps.targets.outputs.mainnet }} | |
| testnet: ${{ steps.targets.outputs.testnet }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - id: targets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mainnet=false | |
| testnet=false | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| case "${{ inputs.environment }}" in | |
| mainnet) mainnet=true ;; | |
| testnet) testnet=true ;; | |
| esac | |
| else | |
| before="${{ github.event.before }}" | |
| if [[ "${before}" =~ ^0+$ ]]; then | |
| diff_base="$(git hash-object -t tree /dev/null)" | |
| else | |
| diff_base="${before}" | |
| fi | |
| changed_files="$(git diff --name-only "${diff_base}" "${GITHUB_SHA}")" | |
| printf '%s\n' "${changed_files}" | |
| while IFS= read -r file; do | |
| case "${file}" in | |
| docker-compose/docker-compose-mainnet.yml|docker-compose/docker-compose-gcp-edge.yml|docker-compose/docker-compose-gcp-edge.override.yml|docker-compose/Caddyfile-gcp-mainnet|docker-compose/Caddyfile-gcp-edge|docker-compose/bens/config.mainnet.template.json|docker-compose/configs/interchain-indexer-chains.json|docker-compose/configs/interchain-indexer-bridges.json|docker-compose/envs/common-blockscout-mainnet.env|docker-compose/envs/common-frontend-scan.env|scripts/render-mainnet-bens.py|.github/scripts/extract-mainnet-aa-verification-inputs.mjs|.github/scripts/mainnet-aa-solc/*|.github/scripts/mainnet-bens-runtime.sh|.github/scripts/mainnet-bens-ui.spec.mjs|.github/scripts/mainnet-aa-source-ui.spec.mjs|.github/scripts/prepare-mainnet-aa-verification.sh|.github/scripts/verify-mainnet-aa-bytecode.mjs|.github/scripts/verify-mainnet-aa-sources.sh) | |
| mainnet=true | |
| ;; | |
| docker-compose/docker-compose-testnet.yml|docker-compose/Caddyfile-gcp-testnet|docker-compose/bens/config.template.json|docker-compose/envs/common-blockscout-testnet.env|docker-compose/envs/common-frontend-testnet.env|scripts/render-testnet-bens.py|scripts/validate-testnet-bens.py|.github/scripts/extract-aa-verification-inputs.mjs|.github/scripts/testnet-bens-ui.spec.mjs|.github/scripts/verify-testnet-aa-sources.sh|.github/scripts/verify-testnet-package.sh|.github/scripts/remove-docker-containers-with-retry.sh) | |
| testnet=true | |
| ;; | |
| docker-compose/bens/*|docker-compose/envs/common-blockscout.env|docker-compose/envs/common-frontend.env|docker-compose/envs/common-smart-contract-verifier.env|docker-compose/envs/common-visualizer.env|scripts/validate-blockscout-env-parity.py) | |
| mainnet=true | |
| testnet=true | |
| ;; | |
| esac | |
| done <<< "${changed_files}" | |
| previous_workflow="$(git show "${diff_base}:.github/workflows/deploy-config.yml" 2>/dev/null || true)" | |
| current_workflow="$(cat .github/workflows/deploy-config.yml)" | |
| previous_mainnet_ref="$(sed -n -E 's/^ DOS_NAMES_MAINNET_SUBGRAPH_REF: ([0-9a-f]{40})$/\1/p' <<< "${previous_workflow}")" | |
| previous_testnet_ref="$(sed -n -E 's/^ DOS_NAMES_TESTNET_SUBGRAPH_REF: ([0-9a-f]{40})$/\1/p' <<< "${previous_workflow}")" | |
| previous_generic_ref="$(sed -n -E 's/^ DOS_NAMES_SUBGRAPH_REF: ([0-9a-f]{40})$/\1/p' <<< "${previous_workflow}")" | |
| previous_mainnet_ref="${previous_mainnet_ref:-${previous_generic_ref}}" | |
| previous_testnet_ref="${previous_testnet_ref:-${previous_generic_ref}}" | |
| current_mainnet_ref="$(sed -n -E 's/^ DOS_NAMES_MAINNET_SUBGRAPH_REF: ([0-9a-f]{40})$/\1/p' <<< "${current_workflow}")" | |
| current_testnet_ref="$(sed -n -E 's/^ DOS_NAMES_TESTNET_SUBGRAPH_REF: ([0-9a-f]{40})$/\1/p' <<< "${current_workflow}")" | |
| if [ "${previous_mainnet_ref}" != "${current_mainnet_ref}" ]; then | |
| mainnet=true | |
| fi | |
| if [ "${previous_testnet_ref}" != "${current_testnet_ref}" ]; then | |
| testnet=true | |
| fi | |
| fi | |
| echo "mainnet=${mainnet}" >> "${GITHUB_OUTPUT}" | |
| echo "testnet=${testnet}" >> "${GITHUB_OUTPUT}" | |
| deploy-mainnet: | |
| needs: changes | |
| if: needs.changes.outputs.mainnet == 'true' | |
| runs-on: ubuntu-latest | |
| environment: mainnet | |
| env: | |
| AA_V07_SOURCE_REPOSITORY: https://github.com/eth-infinitism/account-abstraction.git | |
| AA_V07_SOURCE_REF: 7af70c8993a6f42973f520ae0752386a5032abe7 | |
| KERNEL_SOURCE_REPOSITORY: https://github.com/zerodevapp/kernel.git | |
| KERNEL_SOURCE_REF: cd697c7e21715d015e0643af22310a99aa17433b | |
| KERNEL_SOLADY_REF: 3f2f5345261904463f5429c9031c3d2185c0f4fe | |
| KERNEL_ECDSA_SOURCE_REF: 8f7fd9946b9d351bb5be0428bf34c87bad7ed6c9 | |
| KERNEL_ECDSA_SOLADY_REF: 9deb9ed36a27261a8745db5b7cd7f4cdc3b1cd4e | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate Mainnet and Testnet environment parity | |
| run: python scripts/validate-blockscout-env-parity.py | |
| - name: Validate Testnet BENS configuration | |
| run: python scripts/validate-testnet-bens.py | |
| - name: Validate Mainnet BENS configuration | |
| run: python -m unittest discover -s .github/scripts/tests -p 'test_validate_mainnet_bens.py' -v | |
| - name: Validate Caddy configuration | |
| run: | | |
| set -euo pipefail | |
| CADDY_IMAGE="caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9" | |
| pull_caddy_image() { | |
| for attempt in 1 2 3; do | |
| if docker pull "${CADDY_IMAGE}"; then | |
| return 0 | |
| fi | |
| if [ "${attempt}" -eq 3 ]; then | |
| return 1 | |
| fi | |
| sleep $((attempt * 5)) | |
| done | |
| } | |
| pull_caddy_image | |
| docker run --rm \ | |
| -v "${PWD}/docker-compose/Caddyfile-gcp-edge:/etc/caddy/Caddyfile:ro" \ | |
| "${CADDY_IMAGE}" \ | |
| caddy adapt --config /etc/caddy/Caddyfile >/dev/null | |
| docker run --rm \ | |
| -v "${PWD}/docker-compose/Caddyfile-gcp-mainnet:/etc/caddy/Caddyfile:ro" \ | |
| "${CADDY_IMAGE}" \ | |
| caddy validate --config /etc/caddy/Caddyfile | |
| - name: Prepare immutable Mainnet Account Abstraction verification inputs | |
| run: | | |
| set -euo pipefail | |
| /bin/sh .github/scripts/prepare-mainnet-aa-verification.sh \ | |
| "${AA_V07_SOURCE_REPOSITORY}" \ | |
| "${AA_V07_SOURCE_REF}" \ | |
| "${KERNEL_SOURCE_REPOSITORY}" \ | |
| "${KERNEL_SOURCE_REF}" \ | |
| "${KERNEL_SOLADY_REF}" \ | |
| "${KERNEL_ECDSA_SOURCE_REF}" \ | |
| "${KERNEL_ECDSA_SOLADY_REF}" \ | |
| "${RUNNER_TEMP}/mainnet-aa-verification" \ | |
| "https://main.doschain.com/" | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ env.GCP_SERVICE_ACCOUNT }} | |
| - name: Set up Google Cloud CLI | |
| uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 | |
| with: | |
| project_id: ${{ env.GCP_PROJECT_ID }} | |
| - name: Prepare immutable DOS Names Mainnet runtime package | |
| run: | | |
| set -euo pipefail | |
| checkout="${RUNNER_TEMP}/dos-names-mainnet" | |
| rm -rf "${checkout}" docker-compose/bens/subgraph | |
| git init -q "${checkout}" | |
| git -C "${checkout}" remote add origin "${DOS_NAMES_REPOSITORY}" | |
| git -C "${checkout}" fetch --filter=blob:none origin \ | |
| refs/heads/dos:refs/remotes/origin/dos | |
| git -C "${checkout}" merge-base --is-ancestor \ | |
| "${DOS_NAMES_MAINNET_SUBGRAPH_REF}" refs/remotes/origin/dos | |
| git -C "${checkout}" checkout --detach -q "${DOS_NAMES_MAINNET_SUBGRAPH_REF}" | |
| [ "$(git -C "${checkout}" rev-parse HEAD)" = "${DOS_NAMES_MAINNET_SUBGRAPH_REF}" ] | |
| test -f "${checkout}/subgraph/dos-names/subgraph.template.yaml" | |
| test -f "${checkout}/contracts/deployments/dos-mainnet-7979.json" | |
| cp -a "${checkout}/subgraph/dos-names" docker-compose/bens/subgraph | |
| cp "${checkout}/contracts/deployments/dos-mainnet-7979.json" \ | |
| docker-compose/bens/deployment.json | |
| node docker-compose/bens/subgraph/scripts/render-manifest.mjs \ | |
| docker-compose/bens/deployment.json \ | |
| docker-compose/bens/subgraph/subgraph.template.yaml \ | |
| docker-compose/bens/subgraph/subgraph.yaml | |
| python scripts/render-mainnet-bens.py | |
| echo "DOS_NAMES_SMOKE_NAME=$(jq -er '.smokeName' docker-compose/bens/deployment.json)" >> "${GITHUB_ENV}" | |
| echo "DOS_NAMES_SMOKE_RESOLVED_ADDRESS=$(jq -er '.smokeResolvedAddress' docker-compose/bens/deployment.json)" >> "${GITHUB_ENV}" | |
| - name: Package deployment configuration | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| trap 'rm -f /tmp/common-blockscout-nft-media.env' EXIT | |
| nft_media_json="$( | |
| gcloud secrets versions access latest \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --secret="doscan-nft-media-hmac" | |
| )" | |
| jq -e \ | |
| '.bucket == "doscan" and .host == "storage.googleapis.com"' \ | |
| <<<"${nft_media_json}" >/dev/null | |
| nft_media_access_key_id="$(jq -er '.accessId' <<<"${nft_media_json}")" | |
| nft_media_secret_access_key="$(jq -er '.secret' <<<"${nft_media_json}")" | |
| printf 'NFT_MEDIA_HANDLER_AWS_ACCESS_KEY_ID=%s\nNFT_MEDIA_HANDLER_AWS_SECRET_ACCESS_KEY=%s\n' \ | |
| "${nft_media_access_key_id}" \ | |
| "${nft_media_secret_access_key}" \ | |
| > /tmp/common-blockscout-nft-media.env | |
| unset nft_media_json nft_media_access_key_id nft_media_secret_access_key | |
| tar -czf /tmp/doscan-config.tgz \ | |
| .github/scripts/mainnet-bens-runtime.sh \ | |
| .github/scripts/verify-mainnet-aa-sources.sh \ | |
| docker-compose/docker-compose-mainnet.yml \ | |
| docker-compose/Caddyfile-gcp-mainnet \ | |
| docker-compose/bens \ | |
| docker-compose/configs/interchain-indexer-chains.json \ | |
| docker-compose/configs/interchain-indexer-bridges.json \ | |
| docker-compose/Caddyfile-gcp-edge \ | |
| docker-compose/docker-compose-gcp-edge.yml \ | |
| docker-compose/docker-compose-gcp-edge.override.yml \ | |
| docker-compose/envs/common-blockscout.env \ | |
| docker-compose/envs/common-blockscout-mainnet.env \ | |
| docker-compose/envs/common-frontend.env \ | |
| docker-compose/envs/common-frontend-scan.env \ | |
| docker-compose/envs/common-smart-contract-verifier.env \ | |
| docker-compose/envs/common-visualizer.env \ | |
| -C "${RUNNER_TEMP}" mainnet-aa-verification \ | |
| -C /tmp common-blockscout-nft-media.env | |
| rm -f /tmp/common-blockscout-nft-media.env | |
| - name: Upload configuration through IAP | |
| run: | | |
| set -euo pipefail | |
| gcloud compute scp \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_ZONE}" \ | |
| --tunnel-through-iap \ | |
| /tmp/doscan-config.tgz \ | |
| "${GCP_INSTANCE}:/tmp/doscan-config-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz" | |
| - name: Remove local package containing NFT media credentials | |
| if: always() | |
| run: rm -f /tmp/doscan-config.tgz /tmp/common-blockscout-nft-media.env | |
| - name: Apply configuration and verify services | |
| run: | | |
| set -euo pipefail | |
| gcloud compute ssh "${GCP_INSTANCE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_ZONE}" \ | |
| --tunnel-through-iap \ | |
| --command=' | |
| set -euo pipefail | |
| RUN_ID='"${GITHUB_RUN_ID}"' | |
| RUN_ATTEMPT='"${GITHUB_RUN_ATTEMPT}"' | |
| DEPLOY_ID="${RUN_ID}-${RUN_ATTEMPT}" | |
| ARCHIVE="/tmp/doscan-config-${DEPLOY_ID}.tgz" | |
| test -s "${ARCHIVE}" | |
| SRC="$(mktemp -d /tmp/doscan-deploy.XXXXXX)" | |
| BACKUP="/opt/doscan-l1/backups/github-${DEPLOY_ID}" | |
| L1_PATH="/opt/doscan-l1" | |
| EDGE_PATH="/opt/doscan" | |
| SHARED_ENV_PATH="/opt/doscan/envs" | |
| BLOCKSCOUT_SECRETS="${SHARED_ENV_PATH}/common-blockscout-secrets.env" | |
| NFT_MEDIA_SECRETS="${SRC}/common-blockscout-nft-media.env" | |
| DEPLOYMENT_STARTED=0 | |
| TOUCHED_SERVICES="" | |
| BACKEND_ENV_CHANGED=0 | |
| FRONTEND_ENV_CHANGED=0 | |
| EDGE_TOUCHED=0 | |
| L1_CADDY_CONFIG_CHANGED=0 | |
| cleanup() { | |
| rm -rf "${SRC}" "${ARCHIVE}" | |
| } | |
| verify_frontend_wallets() { | |
| [ $(grep -Pxc "NEXT_PUBLIC_WEB3_WALLETS: \x22\[\x27metamask\x27,\x27rabby\x27,\x27coinbase\x27,\x27token_pocket\x27\]\x22,"<<<"$frontend_env") = 1 ] | |
| } | |
| verify_touched_services() { | |
| cd "${L1_PATH}" | |
| for service in ${TOUCHED_SERVICES}; do | |
| healthy=0 | |
| for attempt in $(seq 1 24); do | |
| container_id="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose ps -q "${service}" | |
| )" | |
| if [ -n "${container_id}" ] && | |
| [ "$(sudo docker inspect -f '"'"'{{.State.Running}}'"'"' "${container_id}")" = "true" ]; then | |
| health_status="$(sudo docker inspect -f '"'"'{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}'"'"' "${container_id}")" | |
| if [ "${health_status}" = "none" ] || [ "${health_status}" = "healthy" ]; then | |
| healthy=1 | |
| break | |
| fi | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${healthy}" -ne 1 ]; then | |
| echo "Service did not become healthy: ${service}" >&2 | |
| return 1 | |
| fi | |
| case "${service}" in | |
| frontend) | |
| sudo docker compose exec -T caddy wget -qO- http://frontend:3000 >/dev/null || return 1 | |
| ;; | |
| smart-contract-verifier|visualizer|sig-provider|user-ops-indexer|interchain-indexer|stats) | |
| sudo docker compose exec -T caddy wget -qO- "http://${service}:8050/health" >/dev/null || return 1 | |
| ;; | |
| esac | |
| done | |
| if grep -Eqw '"'"'backend|user-ops-indexer'"'"' <<<"${TOUCHED_SERVICES}"; then | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://doscan.io/api/v2/proxy/account-abstraction/status >/dev/null || return 1 | |
| fi | |
| } | |
| rollback() { | |
| echo "Deployment failed; restoring configuration from ${BACKUP}" >&2 | |
| set +e | |
| restore_rc=0 | |
| if declare -F bens_rollback >/dev/null 2>&1; then | |
| bens_rollback || restore_rc=1 | |
| fi | |
| cd "${L1_PATH}"; rollback_services="" | |
| for service in ${TOUCHED_SERVICES}; do | |
| if grep -q "^${service} " "${SRC}/old-service-hashes"; then rollback_services="${rollback_services} ${service}"; else sudo docker compose rm -sf "${service}" || restore_rc=1; fi | |
| done; TOUCHED_SERVICES="${rollback_services}" | |
| sudo cp -a "${BACKUP}/compose.yml" "${L1_PATH}/compose.yml" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/Caddyfile-gcp-mainnet" "${L1_PATH}/Caddyfile-gcp-mainnet" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/compose-gcp-edge.yml" "${EDGE_PATH}/compose.yml" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/Caddyfile-gcp-edge" "${EDGE_PATH}/Caddyfile" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/compose.override.yml" "${EDGE_PATH}/compose.override.yml" || restore_rc=1 | |
| if sudo test -f "${BACKUP}/compose.rpc-override.yml"; then | |
| sudo cp -a "${BACKUP}/compose.rpc-override.yml" "${EDGE_PATH}/compose.rpc-override.yml" || restore_rc=1 | |
| else | |
| sudo rm -f "${EDGE_PATH}/compose.rpc-override.yml" || restore_rc=1 | |
| fi | |
| for dir in envs configs; do sudo cp -a "${BACKUP}/${dir}/." "${L1_PATH}/${dir}/" || restore_rc=1; done | |
| sudo cp -a "${BACKUP}/shared-envs/." "${SHARED_ENV_PATH}/" || restore_rc=1 | |
| if [ "${restore_rc}" -ne 0 ]; then | |
| echo "ROLLBACK FAILED; one or more configuration files could not be restored from ${BACKUP}" >&2 | |
| return 1 | |
| fi | |
| cd "${L1_PATH}" | |
| l1_rc=0 | |
| for service in ${TOUCHED_SERVICES}; do | |
| if { [ "${service}" = "backend" ] && [ "${BACKEND_ENV_CHANGED}" -eq 1 ]; } || | |
| { [ "${service}" = "frontend" ] && [ "${FRONTEND_ENV_CHANGED}" -eq 1 ]; }; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --force-recreate --no-deps "${service}" || l1_rc=1 | |
| else | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --no-deps "${service}" || l1_rc=1 | |
| fi | |
| done | |
| if grep -qw caddy <<<"${TOUCHED_SERVICES}" || [ "${L1_CADDY_CONFIG_CHANGED}" -eq 1 ]; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --force-recreate --no-deps caddy || l1_rc=1 | |
| fi | |
| cd "${EDGE_PATH}" | |
| edge_rc=0 | |
| if [ "${EDGE_TOUCHED}" -eq 1 ]; then | |
| sudo docker compose up -d --force-recreate --no-deps caddy || edge_rc=1 | |
| fi | |
| if [ "${l1_rc}" -ne 0 ] || [ "${edge_rc}" -ne 0 ] || | |
| ! bens_wait_restored_core_http || | |
| ! curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://doscan.io/api/v2/stats >/dev/null || | |
| ! curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://doscan.io/public-metrics >/dev/null || | |
| ! verify_touched_services; then | |
| echo "ROLLBACK FAILED; manual recovery is required from ${BACKUP}" >&2 | |
| return 1 | |
| fi | |
| rpc_body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_chainId\",\"params\":[]}" | |
| for rpc_check in \ | |
| "https://main2.doschain.com|archive-doscan-mainnet" \ | |
| "https://main3.doschain.com|archive-doscan-mainnet"; do | |
| rpc_url="${rpc_check%%|*}" | |
| expected_origin="${rpc_check##*|}" | |
| rpc_response="$(curl --fail --silent --show-error --retry 3 --retry-delay 5 \ | |
| -D - -H "Content-Type: application/json" --data "${rpc_body}" "${rpc_url}")" || return 1 | |
| grep -q "\"result\":\"0x1f2b\"" <<<"${rpc_response}" || return 1 | |
| grep -qi "X-DOS-RPC-Origin: ${expected_origin}" <<<"${rpc_response}" || return 1 | |
| done | |
| echo "Rollback completed successfully from ${BACKUP}" >&2 | |
| return 0 | |
| } | |
| finish() { | |
| rc=$? | |
| trap - EXIT | |
| if [ "${rc}" -ne 0 ] && [ "${DEPLOYMENT_STARTED}" -eq 1 ]; then | |
| rollback || rc=2 | |
| fi | |
| cleanup | |
| exit "${rc}" | |
| } | |
| trap finish EXIT | |
| tar -xzf "${ARCHIVE}" -C "${SRC}" | |
| . "${SRC}/.github/scripts/mainnet-bens-runtime.sh" | |
| sudo mkdir -p "${BACKUP}" "${L1_PATH}/envs" "${L1_PATH}/configs" "${EDGE_PATH}" "${SHARED_ENV_PATH}" | |
| sudo cp -a "${L1_PATH}/compose.yml" "${BACKUP}/compose.yml" | |
| sudo cp -a "${L1_PATH}/Caddyfile-gcp-mainnet" "${BACKUP}/Caddyfile-gcp-mainnet" | |
| sudo cp -a "${EDGE_PATH}/compose.yml" "${BACKUP}/compose-gcp-edge.yml" | |
| sudo cp -a "${EDGE_PATH}/Caddyfile" "${BACKUP}/Caddyfile-gcp-edge" | |
| sudo cp -a "${EDGE_PATH}/compose.override.yml" "${BACKUP}/compose.override.yml" | |
| if sudo test -f "${EDGE_PATH}/compose.rpc-override.yml"; then | |
| sudo cp -a "${EDGE_PATH}/compose.rpc-override.yml" "${BACKUP}/compose.rpc-override.yml" | |
| fi | |
| for dir in envs configs; do sudo cp -a "${L1_PATH}/${dir}" "${BACKUP}/${dir}"; done | |
| sudo cp -a "${SHARED_ENV_PATH}" "${BACKUP}/shared-envs" | |
| DEPLOYMENT_STARTED=1 | |
| if ! sudo test -s "${BLOCKSCOUT_SECRETS}"; then | |
| sudo awk -F= \ | |
| '"'"'$1 ~ /^(ACCOUNT_CLOAK_KEY|DATABASE_URL|RELEASE_COOKIE|SECRET_KEY_BASE)$/ {print}'"'"' \ | |
| "${SHARED_ENV_PATH}/common-blockscout.env" | | |
| sudo tee "${BLOCKSCOUT_SECRETS}" >/dev/null | |
| sudo chmod 0600 "${BLOCKSCOUT_SECRETS}" | |
| fi | |
| cd "${L1_PATH}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose config --hash="*" > "${SRC}/old-service-hashes" | |
| bens_prepare | |
| if ! sudo cmp -s "${SRC}/docker-compose/envs/common-blockscout.env" \ | |
| "${L1_PATH}/envs/common-blockscout.env" || | |
| ! sudo cmp -s "${SRC}/docker-compose/envs/common-blockscout-mainnet.env" \ | |
| "${L1_PATH}/envs/common-blockscout-mainnet.env"; then | |
| BACKEND_ENV_CHANGED=1 | |
| fi | |
| if ! sudo cmp -s "${SRC}/docker-compose/envs/common-frontend.env" \ | |
| "${SHARED_ENV_PATH}/common-frontend.env" || | |
| ! sudo cmp -s "${SRC}/docker-compose/envs/common-frontend-scan.env" \ | |
| "${L1_PATH}/envs/common-frontend-scan.env"; then | |
| FRONTEND_ENV_CHANGED=1 | |
| fi | |
| CURRENT_FRONTEND_ENV="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js 2>/dev/null || true | |
| )" | |
| if grep -Fqx "NEXT_PUBLIC_DEX_POOLS_ENABLED=true" \ | |
| "${SRC}/docker-compose/envs/common-frontend.env" && | |
| ! grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${CURRENT_FRONTEND_ENV}"; then | |
| FRONTEND_ENV_CHANGED=1 | |
| fi | |
| if [ "$(grep -Ec "^NFT_MEDIA_HANDLER_AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)=.+$" "${NFT_MEDIA_SECRETS}")" -ne 2 ]; then | |
| echo "NFT media credential file is missing required values" >&2 | |
| exit 1 | |
| fi | |
| CURRENT_NFT_MEDIA_SECRETS="$(mktemp /tmp/doscan-current-nft-media-secrets.XXXXXX)" | |
| sudo grep -E "^NFT_MEDIA_HANDLER_AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)=" \ | |
| "${BLOCKSCOUT_SECRETS}" > "${CURRENT_NFT_MEDIA_SECRETS}" || true | |
| if ! cmp -s "${CURRENT_NFT_MEDIA_SECRETS}" "${NFT_MEDIA_SECRETS}"; then | |
| BACKEND_ENV_CHANGED=1 | |
| fi | |
| rm -f "${CURRENT_NFT_MEDIA_SECRETS}" | |
| MERGED_BLOCKSCOUT_SECRETS="$(mktemp /tmp/doscan-blockscout-secrets.XXXXXX)" | |
| sudo awk -F= \ | |
| "\$1 !~ /^NFT_MEDIA_HANDLER_AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)$/" \ | |
| "${BLOCKSCOUT_SECRETS}" > "${MERGED_BLOCKSCOUT_SECRETS}" | |
| cat "${NFT_MEDIA_SECRETS}" >> "${MERGED_BLOCKSCOUT_SECRETS}" | |
| sudo install -m 0600 "${MERGED_BLOCKSCOUT_SECRETS}" "${BLOCKSCOUT_SECRETS}" | |
| rm -f "${MERGED_BLOCKSCOUT_SECRETS}" | |
| EDGE_CONFIG_CHANGED=0 | |
| EDGE_CADDY_CONFIG_CHANGED=0 | |
| if ! sudo cmp -s "${SRC}/docker-compose/Caddyfile-gcp-mainnet" "${L1_PATH}/Caddyfile-gcp-mainnet"; then | |
| L1_CADDY_CONFIG_CHANGED=1 | |
| fi | |
| if ! sudo cmp -s "${SRC}/docker-compose/docker-compose-gcp-edge.yml" "${EDGE_PATH}/compose.yml" || | |
| ! sudo cmp -s "${SRC}/docker-compose/docker-compose-gcp-edge.override.yml" "${EDGE_PATH}/compose.override.yml"; then | |
| EDGE_CONFIG_CHANGED=1 | |
| fi | |
| EDGE_CADDY_SOURCE_HASH="$(sha256sum "${SRC}/docker-compose/Caddyfile-gcp-edge" | cut -d " " -f1)" | |
| EDGE_CADDY_RUNNING_HASH="$(sudo docker exec caddy sha256sum /etc/caddy/Caddyfile 2>/dev/null | | |
| cut -d " " -f1 || true)" | |
| if [ "${EDGE_CADDY_SOURCE_HASH}" != "${EDGE_CADDY_RUNNING_HASH}" ]; then | |
| EDGE_CADDY_CONFIG_CHANGED=1 | |
| fi | |
| sudo install -m 0644 "${SRC}/docker-compose/docker-compose-mainnet.yml" "${L1_PATH}/compose.yml" | |
| sudo install -m 0644 "${SRC}/docker-compose/Caddyfile-gcp-mainnet" "${L1_PATH}/Caddyfile-gcp-mainnet" | |
| sudo rm -rf "${L1_PATH}/bens" | |
| sudo cp -a "${SRC}/docker-compose/bens" "${L1_PATH}/bens" | |
| sudo chmod 0755 "${L1_PATH}/bens" | |
| sudo chmod 0644 "${L1_PATH}/bens/config.json" | |
| for file in interchain-indexer-{chains,bridges}.json; do sudo install -m 0644 "${SRC}/docker-compose/configs/${file}" "${L1_PATH}/configs/${file}"; done | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-blockscout.env" "${L1_PATH}/envs/common-blockscout.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/docker-compose-gcp-edge.yml" "${EDGE_PATH}/compose.yml" | |
| sudo install -m 0644 "${SRC}/docker-compose/Caddyfile-gcp-edge" "${EDGE_PATH}/Caddyfile" | |
| sudo install -m 0644 "${SRC}/docker-compose/docker-compose-gcp-edge.override.yml" "${EDGE_PATH}/compose.override.yml" | |
| sudo rm -f "${EDGE_PATH}/compose.rpc-override.yml" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-blockscout-mainnet.env" "${L1_PATH}/envs/common-blockscout-mainnet.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-frontend-scan.env" "${L1_PATH}/envs/common-frontend-scan.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-frontend.env" "${SHARED_ENV_PATH}/common-frontend.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-smart-contract-verifier.env" "${SHARED_ENV_PATH}/common-smart-contract-verifier.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-visualizer.env" "${SHARED_ENV_PATH}/common-visualizer.env" | |
| cd "${L1_PATH}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" docker compose config -q | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose config --hash="*" > "${SRC}/new-service-hashes" | |
| TOUCHED_SERVICES="$( | |
| awk '"'"' | |
| NR == FNR { old[$1] = $2; next } | |
| !($1 in old) || old[$1] != $2 { print $1 } | |
| '"'"' "${SRC}/old-service-hashes" "${SRC}/new-service-hashes" | |
| )" | |
| TOUCHED_SERVICES="$(grep -Ev '"'"'^bens(-db|-ipfs|-graph-node|-deployer)?$'"'"' <<<"${TOUCHED_SERVICES}" || true)" | |
| if [ "${BACKEND_ENV_CHANGED}" -eq 1 ] && ! grep -qw backend <<<"${TOUCHED_SERVICES}"; then | |
| TOUCHED_SERVICES="${TOUCHED_SERVICES} backend" | |
| fi | |
| if [ "${FRONTEND_ENV_CHANGED}" -eq 1 ] && ! grep -qw frontend <<<"${TOUCHED_SERVICES}"; then | |
| TOUCHED_SERVICES="${TOUCHED_SERVICES} frontend" | |
| fi | |
| bens_deploy | |
| for service in ${TOUCHED_SERVICES}; do | |
| case "${service}" in | |
| db|db-init|redis-db) | |
| echo "Refusing automatic stateful service change: ${service}" >&2 | |
| exit 1 | |
| ;; | |
| caddy) | |
| continue | |
| ;; | |
| esac | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose pull "${service}" | |
| if { [ "${service}" = "backend" ] && [ "${BACKEND_ENV_CHANGED}" -eq 1 ]; } || | |
| { [ "${service}" = "frontend" ] && [ "${FRONTEND_ENV_CHANGED}" -eq 1 ]; }; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --force-recreate --no-deps "${service}" | |
| else | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --no-deps "${service}" | |
| fi | |
| done | |
| if grep -qw caddy <<<"${TOUCHED_SERVICES}" || [ "${L1_CADDY_CONFIG_CHANGED}" -eq 1 ]; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose up -d --force-recreate --no-deps caddy | |
| fi | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" docker compose exec -T caddy \ | |
| caddy validate --config /etc/caddy/Caddyfile | |
| verify_touched_services | |
| for attempt in $(seq 1 24); do | |
| if curl -fsS http://127.0.0.1:13080/api/v2/stats >/dev/null && | |
| curl -fsS http://127.0.0.1:13080/public-metrics >/dev/null; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 24 ]; then | |
| echo "DOScan did not become healthy in time" >&2 | |
| sudo docker compose ps | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose exec -T backend curl -fsS \ | |
| http://smart-contract-verifier:8050/health >/dev/null | |
| verify_nft_media_runtime() { | |
| probe_key="healthchecks/github-${DEPLOY_ID}.txt" | |
| for probe_attempt in $(seq 1 5); do | |
| if probe_output="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose exec -T backend bin/blockscout rpc \ | |
| "folder = Application.fetch_env!(:nft_media_handler, :r2_folder); key = Path.join(folder, ~s(${probe_key})); body = ~s(doscan-nft-media-runtime); try do true = is_pid(Process.whereis(NFTMediaHandler.Dispatcher)); {:ok, _} = ExAws.S3.put_object(~s(doscan), key, body) |> ExAws.request(); {:ok, %{body: ^body}} = ExAws.S3.get_object(~s(doscan), key) |> ExAws.request(); IO.puts(~s(NFT_MEDIA_RUNTIME_OK)) after ExAws.S3.delete_object(~s(doscan), key) |> ExAws.request() end" | |
| )" && grep -q "NFT_MEDIA_RUNTIME_OK" <<<"${probe_output}"; then | |
| return 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "NFT media runtime probe failed after 5 attempts" >&2 | |
| return 1 | |
| } | |
| verify_nft_media_runtime | |
| sudo docker network inspect doscan-beta >/dev/null 2>&1 || | |
| sudo docker network create doscan-beta >/dev/null | |
| cd "${EDGE_PATH}" | |
| sudo docker compose config -q | |
| if [ "${EDGE_CONFIG_CHANGED}" -eq 1 ] || [ "${EDGE_CADDY_CONFIG_CHANGED}" -eq 1 ]; then | |
| EDGE_TOUCHED=1 | |
| sudo docker compose up -d --force-recreate --no-deps caddy | |
| fi | |
| sudo docker compose exec -T caddy \ | |
| caddy validate --config /etc/caddy/Caddyfile | |
| rpc_body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_chainId\",\"params\":[]}" | |
| check_local_rpc() { | |
| rpc_host="$1" | |
| expected_origin="$2" | |
| rpc_response="$(curl -fsS -D - --max-time 10 \ | |
| -H "Host: ${rpc_host}" \ | |
| -H "Content-Type: application/json" \ | |
| --data "${rpc_body}" \ | |
| http://127.0.0.1:8545/)" | |
| grep -q "\"result\":\"0x1f2b\"" <<<"${rpc_response}" | |
| grep -qi "X-DOS-RPC-Origin: ${expected_origin}" <<<"${rpc_response}" | |
| } | |
| check_local_rpc main2.doschain.com archive-doscan-mainnet | |
| check_local_rpc main3.doschain.com archive-doscan-mainnet | |
| cd "${L1_PATH}" | |
| frontend_env="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" \ | |
| docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js | |
| )" | |
| grep -Fq "NEXT_PUBLIC_METADATA_SERVICE_API_HOST: \"https://doscan.io/metadata-api\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_CONTRACT_INFO_API_HOST: \"https://doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_API_HOST: \"https://doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_PROTOCOLS: \"['"'"'dos-names'"'"']\"" <<<"${frontend_env}" | |
| verify_frontend_wallets | |
| backend_env="$(bens_compose exec -T backend env)" | |
| grep -Fxq "MICROSERVICE_BENS_ENABLED=true" <<<"${backend_env}" | |
| grep -Fxq "MICROSERVICE_BENS_URL=http://bens:8050/" <<<"${backend_env}" | |
| grep -Fxq "MICROSERVICE_BENS_PROTOCOLS=dos-names" <<<"${backend_env}" | |
| smoke_lookup_path="/name-service/api/v1/7979/domains/${SMOKE_NAME}" | |
| curl -fsS http://127.0.0.1:13080/name-service/health | grep -q '"'"'"SERVING"'"'"' | |
| curl -fsS "http://127.0.0.1:13080/name-service/api/v1/protocols?chain_id=7979" | grep -q '"'"'"dos-names"'"'"' | |
| local_domain="$(curl -fsS "http://127.0.0.1:13080${smoke_lookup_path}")" | |
| jq -e --arg name "${SMOKE_NAME}" --arg address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'.name == $name and (.resolved_address.hash | ascii_downcase) == ($address | ascii_downcase)'"'"' \ | |
| <<<"${local_domain}" >/dev/null | |
| local_reverse="$(curl -fsS "http://127.0.0.1:13080/name-service/api/v1/7979/addresses/${SMOKE_RESOLVED_ADDRESS}?protocol_id=dos-names")" | |
| jq -e --arg name "${SMOKE_NAME}" '"'"'.domain.name == $name'"'"' <<<"${local_reverse}" >/dev/null | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 https://doscan.io/api/v2/stats >/dev/null | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 https://doscan.io/public-metrics >/dev/null | |
| expected_token_icon="https://raw.githubusercontent.com/DOS/DOScan-Frontend-Configs/main/configs/token-icons/WDOS.svg" | |
| token_icon_type="$( | |
| curl --fail --silent --show-error --output /dev/null --write-out "%{content_type}" \ | |
| --retry 6 --retry-delay 5 \ | |
| "${expected_token_icon}" | |
| )" | |
| [ "${token_icon_type%%;*}" = "image/svg+xml" ] | |
| for attempt in $(seq 1 30); do | |
| token_info="$( | |
| curl --fail --silent --show-error \ | |
| https://doscan.io/api/v2/tokens/0x1111111111111111111111111111111111111111 | |
| )" | |
| if [ "$(jq -r .icon_url <<<"${token_info}")" = "${expected_token_icon}" ]; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "Mainnet WDOS token icon was not imported from TOKEN_LIST_URL" >&2 | |
| echo "${token_info}" >&2 | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| frontend_env="$( | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://doscan.io/assets/envs.js?deploy=${DEPLOY_ID}" | |
| )" | |
| grep -Fq "NEXT_PUBLIC_METADATA_SERVICE_API_HOST: \"https://doscan.io/metadata-api\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_CONTRACT_INFO_API_HOST: \"https://doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_CROSS_CHAIN_TXS_ENABLED: \"true\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_API_HOST: \"https://doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_PROTOCOLS: \"['"'"'dos-names'"'"']\"" <<<"${frontend_env}" | |
| verify_frontend_wallets | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://doscan.io/api/v1/chains/7979/pools >/dev/null | |
| curl -fsS --retry 6 --retry-delay 5 https://doscan.io/api/v1/status/indexers | grep -Eq '"'"'"name":"avalanche"[^}]*"state":"running"'"'"' | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://doscan.io/name-service/health | grep -q '"'"'"SERVING"'"'"' | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://doscan.io/name-service/api/v1/protocols?chain_id=7979" | grep -q '"'"'"dos-names"'"'"' | |
| public_domain="$(curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://doscan.io${smoke_lookup_path}")" | |
| jq -e --arg name "${SMOKE_NAME}" --arg address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'.name == $name and (.resolved_address.hash | ascii_downcase) == ($address | ascii_downcase)'"'"' \ | |
| <<<"${public_domain}" >/dev/null | |
| public_reverse="$(curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://doscan.io/name-service/api/v1/7979/addresses/${SMOKE_RESOLVED_ADDRESS}?protocol_id=dos-names")" | |
| jq -e --arg name "${SMOKE_NAME}" '"'"'.domain.name == $name'"'"' <<<"${public_reverse}" >/dev/null | |
| search_result="$(curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://doscan.io/api/v2/search?q=${SMOKE_NAME}")" | |
| jq -e --arg name "${SMOKE_NAME}" --arg address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'any(.items[]?; | |
| .type == "ens_domain" and | |
| (.name // .ens_info.name) == $name and | |
| (.address_hash | ascii_downcase) == ($address | ascii_downcase))'"'"' \ | |
| <<<"${search_result}" >/dev/null | |
| DEPLOYMENT_STARTED=0 | |
| /bin/sh "${SRC}/.github/scripts/verify-mainnet-aa-sources.sh" \ | |
| "${SRC}/mainnet-aa-verification" | |
| for metadata_url in \ | |
| "https://doscan.io/metadata-api/api/v1/metadata" \ | |
| "https://doscan.io/api/v1/metadata"; do | |
| metadata_response="$( | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "${metadata_url}?addresses=0x0000000000000000000000000000000000000000&chainId=7979&tagsLimit=1" | |
| )" | |
| grep -q '"'"'"addresses"'"'"' <<<"${metadata_response}" | |
| done | |
| check_public_rpc() { | |
| rpc_url="$1" | |
| expected_origin="$2" | |
| rpc_response="$(curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| -D - \ | |
| -H "Content-Type: application/json" \ | |
| --data "${rpc_body}" \ | |
| "${rpc_url}")" | |
| grep -q "\"result\":\"0x1f2b\"" <<<"${rpc_response}" | |
| grep -qi "X-DOS-RPC-Origin: ${expected_origin}" <<<"${rpc_response}" | |
| } | |
| check_public_rpc https://main2.doschain.com archive-doscan-mainnet | |
| check_public_rpc https://main3.doschain.com archive-doscan-mainnet | |
| curl --fail --silent --show-error --retry 2 --retry-delay 5 \ | |
| https://doscan.io/api/v2/proxy/account-abstraction/status >/dev/null || \ | |
| echo "Account abstraction status is not healthy yet; explorer and RPC deployment succeeded" | |
| sudo docker compose ps caddy | |
| cd "${L1_PATH}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${BLOCKSCOUT_SECRETS}" DOSCAN_BASE_ENV_DIR="${SHARED_ENV_PATH}" docker compose ps | |
| echo "GCP mainnet deployment completed" | |
| ' | |
| - name: Verify Mainnet DOS Name UI with Playwright | |
| env: | |
| DOSCAN_MAINNET_URL: https://doscan.io/ | |
| run: | | |
| set -euo pipefail | |
| playwright_dir="$(mktemp -d "${RUNNER_TEMP}/doscan-mainnet-playwright.XXXXXX")" | |
| trap 'rm -rf "${playwright_dir}"' EXIT | |
| cp .github/scripts/mainnet-bens-ui.spec.mjs "${playwright_dir}/" | |
| cp .github/scripts/mainnet-aa-source-ui.spec.mjs "${playwright_dir}/" | |
| cd "${playwright_dir}" | |
| npm init --yes >/dev/null | |
| npm install --no-save --package-lock=false @playwright/test@1.55.0 >/dev/null | |
| npx playwright install --with-deps chromium >/dev/null | |
| npx playwright test \ | |
| mainnet-bens-ui.spec.mjs \ | |
| mainnet-aa-source-ui.spec.mjs \ | |
| --reporter=line | |
| deploy-testnet: | |
| needs: changes | |
| if: needs.changes.outputs.testnet == 'true' | |
| runs-on: ubuntu-latest | |
| environment: testnet | |
| env: | |
| AA_SOURCE_REPOSITORY: https://github.com/eth-infinitism/account-abstraction.git | |
| AA_SOURCE_REF: 4cbc06072cdc19fd60f285c5997f4f7f57a588de | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate Mainnet and Testnet environment parity | |
| run: python scripts/validate-blockscout-env-parity.py | |
| - name: Validate Testnet BENS configuration | |
| run: python scripts/validate-testnet-bens.py | |
| - name: Validate testnet Caddy configuration | |
| run: | | |
| set -euo pipefail | |
| docker run --rm \ | |
| -v "${PWD}/docker-compose/Caddyfile-gcp-testnet:/etc/caddy/Caddyfile:ro" \ | |
| caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 \ | |
| caddy validate --config /etc/caddy/Caddyfile | |
| - name: Prepare immutable Account Abstraction verification inputs | |
| run: | | |
| set -euo pipefail | |
| checkout="${RUNNER_TEMP}/account-abstraction" | |
| output="${RUNNER_TEMP}/account-abstraction-verification" | |
| rm -rf "${checkout}" "${output}" | |
| git init -q "${checkout}" | |
| git -C "${checkout}" remote add origin "${AA_SOURCE_REPOSITORY}" | |
| git -C "${checkout}" fetch --depth=1 origin "${AA_SOURCE_REF}" | |
| git -C "${checkout}" checkout --detach -q FETCH_HEAD | |
| [ "$(git -C "${checkout}" rev-parse HEAD)" = "${AA_SOURCE_REF}" ] | |
| npm install --global yarn@1.22.22 | |
| yarn --cwd "${checkout}" install --frozen-lockfile --non-interactive | |
| yarn --cwd "${checkout}" compile | |
| node .github/scripts/extract-aa-verification-inputs.mjs \ | |
| "${checkout}" \ | |
| "${output}" | |
| test -s "${output}/manifest.json" | |
| test -s "${output}/entry-point.standard-input.json" | |
| test -s "${output}/simple-account-factory.standard-input.json" | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ env.GCP_SERVICE_ACCOUNT }} | |
| - name: Set up Google Cloud CLI | |
| uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 | |
| with: | |
| project_id: ${{ env.GCP_PROJECT_ID }} | |
| - name: Prepare immutable DOS Names runtime package | |
| run: | | |
| set -euo pipefail | |
| checkout="${RUNNER_TEMP}/dos-names" | |
| rm -rf "${checkout}" docker-compose/bens/subgraph | |
| git init -q "${checkout}" | |
| git -C "${checkout}" remote add origin "${DOS_NAMES_REPOSITORY}" | |
| git -C "${checkout}" fetch --filter=blob:none origin \ | |
| refs/heads/dos:refs/remotes/origin/dos | |
| git -C "${checkout}" merge-base --is-ancestor \ | |
| "${DOS_NAMES_TESTNET_SUBGRAPH_REF}" refs/remotes/origin/dos | |
| git -C "${checkout}" checkout --detach -q "${DOS_NAMES_TESTNET_SUBGRAPH_REF}" | |
| [ "$(git -C "${checkout}" rev-parse HEAD)" = "${DOS_NAMES_TESTNET_SUBGRAPH_REF}" ] | |
| test -f "${checkout}/subgraph/dos-names/subgraph.template.yaml" | |
| test -f "${checkout}/contracts/deployments/dos-testnet-3939.json" | |
| cp -a "${checkout}/subgraph/dos-names" docker-compose/bens/subgraph | |
| cp "${checkout}/contracts/deployments/dos-testnet-3939.json" \ | |
| docker-compose/bens/deployment.json | |
| node docker-compose/bens/subgraph/scripts/render-manifest.mjs \ | |
| docker-compose/bens/deployment.json \ | |
| docker-compose/bens/subgraph/subgraph.template.yaml \ | |
| docker-compose/bens/subgraph/subgraph.yaml | |
| python scripts/render-testnet-bens.py | |
| test -s docker-compose/bens/config.json | |
| echo "DOS_NAMES_SMOKE_NAME=$(jq -er '.smokeName' docker-compose/bens/deployment.json)" >> "${GITHUB_ENV}" | |
| echo "DOS_NAMES_SMOKE_RESOLVED_ADDRESS=$(jq -er '.smokeResolvedAddress' docker-compose/bens/deployment.json)" >> "${GITHUB_ENV}" | |
| - name: Package testnet deployment configuration | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| trap 'rm -f /tmp/common-blockscout-nft-media.env' EXIT | |
| nft_media_json="$( | |
| gcloud secrets versions access latest \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --secret="doscan-nft-media-hmac" | |
| )" | |
| jq -e \ | |
| '.bucket == "doscan" and .host == "storage.googleapis.com"' \ | |
| <<<"${nft_media_json}" >/dev/null | |
| nft_media_access_key_id="$(jq -er '.accessId' <<<"${nft_media_json}")" | |
| nft_media_secret_access_key="$(jq -er '.secret' <<<"${nft_media_json}")" | |
| printf 'NFT_MEDIA_HANDLER_AWS_ACCESS_KEY_ID=%s\nNFT_MEDIA_HANDLER_AWS_SECRET_ACCESS_KEY=%s\n' \ | |
| "${nft_media_access_key_id}" \ | |
| "${nft_media_secret_access_key}" \ | |
| > /tmp/common-blockscout-nft-media.env | |
| unset nft_media_json nft_media_access_key_id nft_media_secret_access_key | |
| tar -czf /tmp/doscan-testnet-config.tgz \ | |
| .github/scripts/retry-public-http.sh \ | |
| .github/scripts/retry-testnet-rpc.sh \ | |
| .github/scripts/remove-docker-containers-with-retry.sh \ | |
| .github/scripts/verify-testnet-aa-sources.sh \ | |
| docker-compose/docker-compose-testnet.yml \ | |
| docker-compose/Caddyfile-gcp-testnet \ | |
| docker-compose/bens \ | |
| docker-compose/envs/common-blockscout.env \ | |
| docker-compose/envs/common-blockscout-testnet.env \ | |
| docker-compose/envs/common-frontend.env \ | |
| docker-compose/envs/common-frontend-testnet.env \ | |
| docker-compose/envs/common-smart-contract-verifier.env \ | |
| docker-compose/envs/common-visualizer.env \ | |
| -C /tmp common-blockscout-nft-media.env \ | |
| -C "${RUNNER_TEMP}" account-abstraction-verification | |
| bash .github/scripts/verify-testnet-package.sh /tmp/doscan-testnet-config.tgz | |
| rm -f /tmp/common-blockscout-nft-media.env | |
| - name: Upload testnet configuration through IAP | |
| run: | | |
| set -euo pipefail | |
| cloudsdk_python="$(gcloud info --format='value(basic.python_location)')" | |
| "${cloudsdk_python}" -m pip install numpy | |
| export CLOUDSDK_PYTHON_SITEPACKAGES=1 | |
| uploaded=0 | |
| for attempt in 1 2 3; do | |
| if timeout --signal=KILL 90s gcloud compute scp \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_TESTNET_ZONE}" \ | |
| --tunnel-through-iap \ | |
| --scp-flag=-O \ | |
| /tmp/doscan-testnet-config.tgz \ | |
| "${GCP_TESTNET_INSTANCE}:/tmp/doscan-testnet-config-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz"; then | |
| uploaded=1 | |
| break | |
| fi | |
| if [ "${attempt}" -eq 3 ]; then | |
| echo "Testnet configuration upload through IAP failed after ${attempt} attempts" >&2 | |
| exit 1 | |
| fi | |
| sleep $((attempt * 5)) | |
| done | |
| test "${uploaded}" -eq 1 | |
| - name: Remove local testnet package containing NFT media credentials | |
| if: always() | |
| run: rm -f /tmp/doscan-testnet-config.tgz /tmp/common-blockscout-nft-media.env | |
| - name: Apply testnet configuration and verify services | |
| run: | | |
| set -euo pipefail | |
| gcloud compute ssh "${GCP_TESTNET_INSTANCE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_TESTNET_ZONE}" \ | |
| --tunnel-through-iap \ | |
| --command=' | |
| set -euo pipefail | |
| umask 077 | |
| RUN_ID='"${GITHUB_RUN_ID}"' | |
| RUN_ATTEMPT='"${GITHUB_RUN_ATTEMPT}"' | |
| DEPLOY_ID="${RUN_ID}-${RUN_ATTEMPT}" | |
| ARCHIVE="/tmp/doscan-testnet-config-${DEPLOY_ID}.tgz" | |
| test -s "${ARCHIVE}" | |
| SRC="$(mktemp -d /tmp/doscan-testnet-deploy.XXXXXX)" | |
| DEPLOY_PATH="/opt/doscan-testnet" | |
| BACKUP="${DEPLOY_PATH}/backups/github-${DEPLOY_ID}" | |
| SECRETS_ENV="${DEPLOY_PATH}/envs/common-blockscout-secrets.env" | |
| BENS_SECRETS_ENV="${DEPLOY_PATH}/envs/bens-secrets.env" | |
| NFT_MEDIA_SECRETS="${SRC}/common-blockscout-nft-media.env" | |
| BENS_DB_VOLUME="doscan-testnet_bens_postgres_data" | |
| BENS_IPFS_VOLUME="doscan-testnet_bens_ipfs_data" | |
| BLOCKSCOUT_DB="blockscout_jasj_20260809" | |
| BENS_PREDEPLOY_STOPPED=0 | |
| DEPLOYMENT_STARTED=0 | |
| cleanup() { | |
| rm -rf "${SRC}" "${ARCHIVE}" | |
| } | |
| verify_frontend_wallets() { | |
| [ $(grep -Pxc "NEXT_PUBLIC_WEB3_WALLETS: \x22\[\x27metamask\x27,\x27rabby\x27,\x27coinbase\x27,\x27token_pocket\x27\]\x22,"<<<"$frontend_env") = 1 ] | |
| } | |
| rollback() { | |
| echo "Testnet deployment failed; restoring ${BACKUP}" >&2 | |
| set +e | |
| restore_rc=0 | |
| cd "${DEPLOY_PATH}" | |
| current_services="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose config --services 2>/dev/null || true | |
| )" | |
| for service in bens bens-graph-node bens-ipfs bens-db; do | |
| if grep -qx "${service}" <<<"${current_services}"; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose rm -sf "${service}" || restore_rc=1 | |
| fi | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose stop backend stats || restore_rc=1 | |
| if ! user_ops_container_ids="$( | |
| sudo docker ps -aq \ | |
| --filter "label=com.docker.compose.project=doscan-testnet" \ | |
| --filter "label=com.docker.compose.service=user-ops-indexer" | |
| )"; then | |
| restore_rc=1 | |
| fi | |
| if [ -n "${user_ops_container_ids}" ]; then | |
| sudo docker stop ${user_ops_container_ids} || restore_rc=1 | |
| sudo bash "${SRC}/.github/scripts/remove-docker-containers-with-retry.sh" \ | |
| ${user_ops_container_ids} || restore_rc=1 | |
| fi | |
| if [ "${restore_rc}" -ne 0 ]; then | |
| echo "TESTNET ROLLBACK ABORTED; database writers could not be stopped" >&2 | |
| return 1 | |
| fi | |
| sudo docker compose exec -T db dropdb --force -U postgres "${BLOCKSCOUT_DB}" || restore_rc=1 | |
| sudo docker compose exec -T db createdb -U postgres "${BLOCKSCOUT_DB}" || restore_rc=1 | |
| sudo cat "${BACKUP}/blockscout.dump" | | |
| sudo docker compose exec -T db pg_restore -U postgres -d "${BLOCKSCOUT_DB}" --no-owner --no-privileges || | |
| restore_rc=1 | |
| sudo docker compose exec -T db dropdb --force -U postgres stats || restore_rc=1 | |
| sudo docker compose exec -T db createdb -U postgres stats || restore_rc=1 | |
| sudo cat "${BACKUP}/stats.dump" | | |
| sudo docker compose exec -T db pg_restore -U postgres -d stats --no-owner --no-privileges || | |
| restore_rc=1 | |
| sudo cp -a "${BACKUP}/docker-compose.yml" "${DEPLOY_PATH}/docker-compose.yml" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/Caddyfile" "${DEPLOY_PATH}/Caddyfile" || restore_rc=1 | |
| if sudo test -f "${BACKUP}/Caddyfile-gcp-testnet"; then | |
| sudo cp -a "${BACKUP}/Caddyfile-gcp-testnet" "${DEPLOY_PATH}/Caddyfile-gcp-testnet" || | |
| restore_rc=1 | |
| else | |
| sudo rm -f "${DEPLOY_PATH}/Caddyfile-gcp-testnet" || restore_rc=1 | |
| fi | |
| sudo cp -a "${BACKUP}/envs/." "${DEPLOY_PATH}/envs/" || restore_rc=1 | |
| if sudo test -f "${BACKUP}/bens-existed"; then | |
| sudo rm -rf "${DEPLOY_PATH}/bens" || restore_rc=1 | |
| sudo cp -a "${BACKUP}/bens" "${DEPLOY_PATH}/bens" || restore_rc=1 | |
| else | |
| sudo rm -rf "${DEPLOY_PATH}/bens" || restore_rc=1 | |
| fi | |
| if sudo test -f "${BACKUP}/bens-state-existed"; then | |
| sudo test -s "${BACKUP}/bens-graph-node.dump" || restore_rc=1 | |
| sudo test -s "${BACKUP}/bens-ipfs.tgz" || restore_rc=1 | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose up -d bens-db || restore_rc=1 | |
| for attempt in $(seq 1 20); do | |
| if sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db pg_isready -U graph-node -d graph-node; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 20 ]; then | |
| restore_rc=1 | |
| fi | |
| sleep 3 | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db dropdb --force -U graph-node graph-node || | |
| restore_rc=1 | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db createdb -U graph-node graph-node || | |
| restore_rc=1 | |
| sudo cat "${BACKUP}/bens-graph-node.dump" | | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db pg_restore -U graph-node -d graph-node \ | |
| --no-owner --no-privileges || | |
| restore_rc=1 | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose stop bens-db || restore_rc=1 | |
| sudo docker run --rm --user 0:0 --entrypoint /bin/sh \ | |
| -v "${BENS_IPFS_VOLUME}:/data/ipfs" \ | |
| -v "${BACKUP}:/backup:ro" \ | |
| ipfs/kubo:v0.43.0@sha256:63f5502f7a01b82a675e45bae81b2f5dfa90248ec4a86cd0a58c218347e1f2d2 \ | |
| -ec '"'"'find /data/ipfs -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +; tar -C /data/ipfs -xzf /backup/bens-ipfs.tgz'"'"' || | |
| restore_rc=1 | |
| else | |
| for volume in "${BENS_DB_VOLUME}" "${BENS_IPFS_VOLUME}"; do | |
| if sudo docker volume inspect "${volume}" >/dev/null 2>&1; then | |
| sudo docker volume rm "${volume}" || restore_rc=1 | |
| fi | |
| done | |
| fi | |
| if [ "${restore_rc}" -ne 0 ]; then | |
| echo "TESTNET ROLLBACK FAILED; manual recovery is required from ${BACKUP}" >&2 | |
| return 1 | |
| fi | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" docker compose up -d || restore_rc=1 | |
| if [ "${FRONTEND_ENV_CHANGED:-0}" -eq 1 ]; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose up -d --force-recreate --no-deps frontend || restore_rc=1 | |
| restored_frontend_env="" | |
| frontend_env_restored=0 | |
| for attempt in $(seq 1 12); do | |
| if restored_frontend_env="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js | |
| )"; then | |
| frontend_env_restored=1 | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${frontend_env_restored}" -ne 1 ]; then | |
| restore_rc=1 | |
| fi | |
| restored_dex_value="$( | |
| sudo awk -F= \ | |
| '"'"'$1 == "NEXT_PUBLIC_DEX_POOLS_ENABLED" { value = $2 } END { print value }'"'"' \ | |
| "${DEPLOY_PATH}/envs/common-frontend.env" \ | |
| "${DEPLOY_PATH}/envs/common-frontend-testnet.env" | |
| )" | |
| if [ "${restored_dex_value}" = "true" ]; then | |
| grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${restored_frontend_env}" || | |
| restore_rc=1 | |
| else | |
| if grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${restored_frontend_env}"; then | |
| restore_rc=1 | |
| fi | |
| fi | |
| fi | |
| if [ "${restore_rc}" -ne 0 ]; then | |
| echo "TESTNET ROLLBACK FAILED; restored frontend runtime is unhealthy" >&2 | |
| return 1 | |
| fi | |
| restored_stack_healthy=0 | |
| for attempt in $(seq 1 30); do | |
| if curl -fsS http://127.0.0.1:13080/api/v2/stats >/dev/null; then | |
| restored_stack_healthy=1 | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${restored_stack_healthy}" -ne 1 ]; then | |
| echo "TESTNET ROLLBACK FAILED; restored stack is not healthy" >&2 | |
| return 1 | |
| fi | |
| if sudo test -f "${BACKUP}/bens-existed" && | |
| sudo test -f "${BACKUP}/bens-state-existed"; then | |
| old_deployment="${BACKUP}/bens/deployment.json" | |
| if ! sudo jq -e \ | |
| '"'"'.chainId == 3939 and | |
| (.finalDeploymentBlock | type == "number") and | |
| (.smokeName | type == "string") and | |
| (.smokeResolvedAddress | type == "string")'"'"' \ | |
| "${old_deployment}" >/dev/null; then | |
| echo "TESTNET ROLLBACK FAILED; prior BENS deployment metadata is invalid" >&2 | |
| return 1 | |
| fi | |
| old_final_block="$(sudo jq -er '"'"'.finalDeploymentBlock'"'"' "${old_deployment}")" | |
| old_smoke_name="$(sudo jq -er '"'"'.smokeName'"'"' "${old_deployment}")" | |
| old_smoke_address="$(sudo jq -er '"'"'.smokeResolvedAddress'"'"' "${old_deployment}")" | |
| restored_bens_healthy=0 | |
| for attempt in $(seq 1 30); do | |
| restored_graph_status="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -fsS \ | |
| -H "Content-Type: application/json" \ | |
| --data-binary '"'"'{"query":"{ _meta { block { number } hasIndexingErrors } }"}'"'"' \ | |
| http://bens-graph-node:8000/subgraphs/name/dos-names 2>/dev/null || true | |
| )" | |
| restored_smoke_response="$( | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/name-service/api/v1/3939/domains/${old_smoke_name}" 2>/dev/null || true | |
| )" | |
| restored_reverse_response="$( | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/name-service/api/v1/3939/addresses/${old_smoke_address}?protocol_id=dos-names" 2>/dev/null || true | |
| )" | |
| restored_search_response="$( | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/api/v2/search?q=${old_smoke_name}" 2>/dev/null || true | |
| )" | |
| if curl -fsS -H "Host: test.doscan.io" \ | |
| http://127.0.0.1:13080/name-service/health | grep -q '"'"'"SERVING"'"'"' && | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/name-service/api/v1/protocols?chain_id=3939" | grep -q '"'"'"dos-names"'"'"' && | |
| jq -e --argjson final_block "${old_final_block}" \ | |
| '"'"'.data._meta.hasIndexingErrors == false and | |
| (.data._meta.block.number | tonumber) >= $final_block'"'"' \ | |
| <<<"${restored_graph_status}" >/dev/null 2>&1 && | |
| jq -e --arg name "${old_smoke_name}" --arg address "${old_smoke_address}" \ | |
| '"'"'.name == $name and | |
| (.resolved_address.hash | ascii_downcase) == ($address | ascii_downcase)'"'"' \ | |
| <<<"${restored_smoke_response}" >/dev/null 2>&1 && | |
| jq -e --arg name "${old_smoke_name}" \ | |
| '"'"'.domain.name == $name'"'"' <<<"${restored_reverse_response}" >/dev/null 2>&1 && | |
| jq -e --arg name "${old_smoke_name}" --arg address "${old_smoke_address}" \ | |
| '"'"'any(.items[]; | |
| .type == "ens_domain" and (.name // .ens_info.name) == $name and | |
| (.address_hash | ascii_downcase) == ($address | ascii_downcase))'"'"' \ | |
| <<<"${restored_search_response}" >/dev/null 2>&1; then | |
| restored_bens_healthy=1 | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${restored_bens_healthy}" -ne 1 ]; then | |
| echo "TESTNET ROLLBACK FAILED; restored BENS protocol is not healthy" >&2 | |
| return 1 | |
| fi | |
| fi | |
| echo "Testnet rollback completed successfully" >&2 | |
| return 0 | |
| } | |
| finish() { | |
| rc=$? | |
| trap - EXIT | |
| if [ "${rc}" -ne 0 ] && [ "${DEPLOYMENT_STARTED}" -eq 1 ]; then | |
| rollback || rc=2 | |
| elif [ "${rc}" -ne 0 ] && [ "${BENS_PREDEPLOY_STOPPED}" -eq 1 ]; then | |
| cd "${DEPLOY_PATH}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose up -d bens-db bens-ipfs bens-graph-node bens || rc=2 | |
| fi | |
| cleanup | |
| exit "${rc}" | |
| } | |
| trap finish EXIT | |
| tar -xzf "${ARCHIVE}" -C "${SRC}" | |
| . "${SRC}/.github/scripts/retry-testnet-rpc.sh" | |
| DEPLOYMENT_JSON="${SRC}/docker-compose/bens/deployment.json" | |
| jq -e \ | |
| '"'"'.chainId == 3939 and | |
| (.deploymentBlock | type == "number" and . >= 0) and | |
| (.finalDeploymentBlock | type == "number") and | |
| (.finalDeploymentBlock >= .deploymentBlock) and | |
| .smokeName == "bens-smoke.dos" and | |
| (.smokeResolvedAddress | type == "string" and | |
| test("^0x[0-9a-fA-F]{40}$") and | |
| ascii_downcase != "0x0000000000000000000000000000000000000000") and | |
| ([.contracts.dosRegistry, .contracts.dosRegistrar, | |
| .contracts.permissionedResolverImplementation, | |
| .contracts.rootRegistry] | all(type == "string"))'"'"' \ | |
| "${DEPLOYMENT_JSON}" >/dev/null | |
| DEPLOYMENT_BLOCK="$(jq -er ".deploymentBlock" "${DEPLOYMENT_JSON}")" | |
| FINAL_DEPLOYMENT_BLOCK="$(jq -er ".finalDeploymentBlock" "${DEPLOYMENT_JSON}")" | |
| SMOKE_NAME="$(jq -er ".smokeName" "${DEPLOYMENT_JSON}")" | |
| SMOKE_RESOLVED_ADDRESS="$(jq -er ".smokeResolvedAddress" "${DEPLOYMENT_JSON}")" | |
| TESTNET_RPC_CONTRACT_CODE_URL="https://test.doschain.com/" | |
| while read -r contract_name contract_address; do | |
| rpc_body="$( | |
| jq -cn --arg address "${contract_address}" \ | |
| '"'"'{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":[$address,"latest"]}'"'"' | |
| )" | |
| contract_code="$( | |
| testnet_rpc_request "${rpc_body}" 0 "${TESTNET_RPC_CONTRACT_CODE_URL}" | | |
| jq -er ".result" | |
| )" | |
| if [ "${contract_code}" = "0x" ] || [ "${contract_code}" = "0x0" ]; then | |
| echo "DOS Names ${contract_name} has no Testnet bytecode at ${contract_address}" >&2 | |
| exit 1 | |
| fi | |
| done < <(jq -er '"'"'.contracts | to_entries[] | "\(.key) \(.value)"'"'"' "${DEPLOYMENT_JSON}") | |
| sudo mkdir -p "${BACKUP}" "${DEPLOY_PATH}/envs" | |
| sudo cp -a "${DEPLOY_PATH}/docker-compose.yml" "${BACKUP}/docker-compose.yml" 2>/dev/null || true | |
| sudo cp -a "${DEPLOY_PATH}/Caddyfile" "${BACKUP}/Caddyfile" 2>/dev/null || true | |
| sudo cp -a "${DEPLOY_PATH}/Caddyfile-gcp-testnet" "${BACKUP}/Caddyfile-gcp-testnet" 2>/dev/null || true | |
| sudo cp -a "${DEPLOY_PATH}/envs" "${BACKUP}/envs" 2>/dev/null || true | |
| if sudo test -d "${DEPLOY_PATH}/bens"; then | |
| sudo cp -a "${DEPLOY_PATH}/bens" "${BACKUP}/bens" | |
| sudo touch "${BACKUP}/bens-existed" | |
| fi | |
| if ! sudo test -s "${SECRETS_ENV}"; then | |
| sudo awk -F= \ | |
| '"'"'$1 ~ /^(ACCOUNT_CLOAK_KEY|DATABASE_URL|RELEASE_COOKIE|SECRET_KEY_BASE)$/ {print}'"'"' \ | |
| "${DEPLOY_PATH}/envs/common-blockscout-testnet.env" | | |
| sudo tee "${SECRETS_ENV}" >/dev/null | |
| sudo chmod 0600 "${SECRETS_ENV}" | |
| fi | |
| if ! sudo grep -Eq "^SECRET_KEY_BASE=.+" "${SECRETS_ENV}"; then | |
| echo "Existing Testnet SECRET_KEY_BASE is missing; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| blockscout_database_url="$( | |
| sudo awk -F= \ | |
| "\$1 == \"DATABASE_URL\" {sub(/^[^=]*=/, \"\"); print; exit}" \ | |
| "${SECRETS_ENV}" | |
| )" | |
| blockscout_database_target="${blockscout_database_url%%\?*}" | |
| if [ "${blockscout_database_target##*/}" != "${BLOCKSCOUT_DB}" ]; then | |
| echo "Testnet DATABASE_URL must target ${BLOCKSCOUT_DB}; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| canonical_bens_key_count="$( | |
| sudo grep -Ec '"'"'^DOSCAN_BENS_DB_PASSWORD='"'"' "${SECRETS_ENV}" || true | |
| )" | |
| canonical_bens_password_count="$( | |
| sudo grep -Ec '"'"'^DOSCAN_BENS_DB_PASSWORD=[0-9a-f]{64}$'"'"' "${SECRETS_ENV}" || true | |
| )" | |
| if [ "${canonical_bens_key_count}" -eq 0 ]; then | |
| if sudo test -s "${BENS_SECRETS_ENV}"; then | |
| bens_db_password="$( | |
| sudo awk -F= '"'"'$1 == "POSTGRES_PASSWORD" { print $2 }'"'"' "${BENS_SECRETS_ENV}" | |
| )" | |
| else | |
| bens_db_password="$(openssl rand -hex 32)" | |
| fi | |
| if ! grep -Eq '"'"'^[0-9a-f]{64}$'"'"' <<<"${bens_db_password}"; then | |
| echo "Existing Testnet BENS database password is invalid; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| canonical_secrets_tmp="$(mktemp /tmp/doscan-testnet-secrets.XXXXXX)" | |
| sudo cat "${SECRETS_ENV}" > "${canonical_secrets_tmp}" | |
| printf '"'"'DOSCAN_BENS_DB_PASSWORD=%s\n'"'"' "${bens_db_password}" >> "${canonical_secrets_tmp}" | |
| sudo install -m 0600 "${canonical_secrets_tmp}" "${SECRETS_ENV}" | |
| rm -f "${canonical_secrets_tmp}" | |
| unset bens_db_password | |
| elif [ "${canonical_bens_key_count}" -ne 1 ] || | |
| [ "${canonical_bens_password_count}" -ne 1 ]; then | |
| echo "Testnet canonical BENS database password is missing or duplicated; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| bens_db_password="$( | |
| sudo awk -F= '"'"'$1 == "DOSCAN_BENS_DB_PASSWORD" { print $2 }'"'"' "${SECRETS_ENV}" | |
| )" | |
| if ! grep -Eq '"'"'^[0-9a-f]{64}$'"'"' <<<"${bens_db_password}"; then | |
| echo "Testnet canonical BENS database password is invalid; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| bens_secrets_tmp="$(mktemp /tmp/doscan-testnet-bens-secrets.XXXXXX)" | |
| printf \ | |
| '"'"'POSTGRES_PASSWORD=%s\npostgres_pass=%s\nBENS__DATABASE__CONNECT__URL=postgresql://graph-node:%s@bens-db:5432/graph-node\n'"'"' \ | |
| "${bens_db_password}" "${bens_db_password}" "${bens_db_password}" > "${bens_secrets_tmp}" | |
| sudo install -m 0600 "${bens_secrets_tmp}" "${BENS_SECRETS_ENV}" | |
| rm -f "${bens_secrets_tmp}" | |
| unset bens_db_password | |
| if [ "$(sudo grep -Ec '"'"'^(POSTGRES_PASSWORD|postgres_pass|BENS__DATABASE__CONNECT__URL)=.+$'"'"' "${BENS_SECRETS_ENV}")" -ne 3 ]; then | |
| echo "BENS database secrets derived from canonical password are incomplete; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| cd "${DEPLOY_PATH}" | |
| FRONTEND_ENV_CHANGED=0 | |
| if ! sudo cmp -s "${SRC}/docker-compose/envs/common-frontend.env" \ | |
| "${DEPLOY_PATH}/envs/common-frontend.env" || | |
| ! sudo cmp -s "${SRC}/docker-compose/envs/common-frontend-testnet.env" \ | |
| "${DEPLOY_PATH}/envs/common-frontend-testnet.env"; then | |
| FRONTEND_ENV_CHANGED=1 | |
| fi | |
| CURRENT_FRONTEND_ENV="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js 2>/dev/null || true | |
| )" | |
| if grep -Fqx "NEXT_PUBLIC_DEX_POOLS_ENABLED=true" \ | |
| "${SRC}/docker-compose/envs/common-frontend.env" && | |
| ! grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${CURRENT_FRONTEND_ENV}"; then | |
| FRONTEND_ENV_CHANGED=1 | |
| fi | |
| current_volume="$( | |
| sudo docker inspect doscan-testnet-db-1 \ | |
| --format '"'"'{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}'"'"' | |
| )" | |
| if [ "${current_volume}" != "doscan-testnet_postgres_data" ]; then | |
| echo "Unexpected Testnet Postgres volume: ${current_volume}" >&2 | |
| exit 1 | |
| fi | |
| cd "${DEPLOY_PATH}" | |
| bens_db_exists="$(sudo docker volume inspect "${BENS_DB_VOLUME}" >/dev/null 2>&1 && echo 1 || echo 0)" | |
| bens_ipfs_exists="$(sudo docker volume inspect "${BENS_IPFS_VOLUME}" >/dev/null 2>&1 && echo 1 || echo 0)" | |
| if [ "${bens_db_exists}" != "${bens_ipfs_exists}" ]; then | |
| echo "Testnet BENS volumes are incomplete; refusing deployment" >&2 | |
| exit 1 | |
| fi | |
| if [ "${bens_db_exists}" -eq 1 ]; then | |
| existing_services="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose config --services | |
| )" | |
| for service in bens-db bens-ipfs bens-graph-node bens; do | |
| if ! grep -qx "${service}" <<<"${existing_services}"; then | |
| echo "BENS volume exists but current Compose lacks ${service}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose up -d bens-db bens-ipfs | |
| for attempt in $(seq 1 20); do | |
| if sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db pg_isready -U graph-node -d graph-node; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 20 ]; then | |
| echo "Existing BENS database did not become ready" >&2 | |
| sudo docker compose logs --tail=100 bens-db bens-ipfs | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| BENS_PREDEPLOY_STOPPED=1 | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose stop bens bens-graph-node | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db pg_dump -U graph-node -Fc graph-node | | |
| sudo tee "${BACKUP}/bens-graph-node.dump" >/dev/null | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose stop bens-db bens-ipfs | |
| sudo docker run --rm --user 0:0 --entrypoint /bin/sh \ | |
| -v "${BENS_IPFS_VOLUME}:/data/ipfs:ro" \ | |
| -v "${BACKUP}:/backup" \ | |
| ipfs/kubo:v0.43.0@sha256:63f5502f7a01b82a675e45bae81b2f5dfa90248ec4a86cd0a58c218347e1f2d2 \ | |
| -ec "tar -C /data/ipfs -czf /backup/bens-ipfs.tgz ." | |
| sudo test -s "${BACKUP}/bens-graph-node.dump" | |
| sudo test -s "${BACKUP}/bens-ipfs.tgz" | |
| sudo touch "${BACKUP}/bens-state-existed" | |
| fi | |
| sudo docker compose exec -T db pg_dump -U postgres -Fc "${BLOCKSCOUT_DB}" | | |
| sudo tee "${BACKUP}/blockscout.dump" >/dev/null | |
| sudo docker compose exec -T db pg_dump -U postgres -Fc stats | | |
| sudo tee "${BACKUP}/stats.dump" >/dev/null | |
| sudo test -s "${BACKUP}/blockscout.dump" | |
| sudo test -s "${BACKUP}/stats.dump" | |
| DEPLOYMENT_STARTED=1 | |
| BENS_PREDEPLOY_STOPPED=0 | |
| if [ "$(grep -Ec "^NFT_MEDIA_HANDLER_AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)=.+$" "${NFT_MEDIA_SECRETS}")" -ne 2 ]; then | |
| echo "NFT media credential file is missing required values" >&2 | |
| exit 1 | |
| fi | |
| MERGED_BLOCKSCOUT_SECRETS="$(mktemp /tmp/doscan-testnet-blockscout-secrets.XXXXXX)" | |
| sudo awk -F= \ | |
| "\$1 !~ /^NFT_MEDIA_HANDLER_AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY)$/" \ | |
| "${SECRETS_ENV}" > "${MERGED_BLOCKSCOUT_SECRETS}" | |
| cat "${NFT_MEDIA_SECRETS}" >> "${MERGED_BLOCKSCOUT_SECRETS}" | |
| sudo install -m 0600 "${MERGED_BLOCKSCOUT_SECRETS}" "${SECRETS_ENV}" | |
| rm -f "${MERGED_BLOCKSCOUT_SECRETS}" | |
| sudo install -m 0644 "${SRC}/docker-compose/docker-compose-testnet.yml" "${DEPLOY_PATH}/docker-compose.yml" | |
| sudo install -m 0644 "${SRC}/docker-compose/Caddyfile-gcp-testnet" "${DEPLOY_PATH}/Caddyfile-gcp-testnet" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-blockscout.env" "${DEPLOY_PATH}/envs/common-blockscout.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-blockscout-testnet.env" "${DEPLOY_PATH}/envs/common-blockscout-testnet.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-frontend.env" "${DEPLOY_PATH}/envs/common-frontend.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-frontend-testnet.env" "${DEPLOY_PATH}/envs/common-frontend-testnet.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-smart-contract-verifier.env" "${DEPLOY_PATH}/envs/common-smart-contract-verifier.env" | |
| sudo install -m 0644 "${SRC}/docker-compose/envs/common-visualizer.env" "${DEPLOY_PATH}/envs/common-visualizer.env" | |
| sudo rm -rf "${DEPLOY_PATH}/bens" | |
| sudo cp -a "${SRC}/docker-compose/bens" "${DEPLOY_PATH}/bens" | |
| sudo chmod 0755 "${DEPLOY_PATH}/bens" | |
| sudo chmod 0644 "${DEPLOY_PATH}/bens/config.json" | |
| cd "${DEPLOY_PATH}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" docker compose config -q | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose up -d bens-db bens-ipfs | |
| for attempt in $(seq 1 30); do | |
| if sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose exec -T bens-db pg_isready -U graph-node -d graph-node; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "BENS database did not become ready" >&2 | |
| sudo docker compose logs --tail=100 bens-db bens-ipfs | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| DOSCAN_BENS_SECRETS_ENV="${BENS_SECRETS_ENV}" \ | |
| docker compose up -d bens-graph-node | |
| for attempt in $(seq 1 30); do | |
| if sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -sS --max-time 5 \ | |
| http://bens-graph-node:8020/ >/dev/null; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "BENS Graph Node did not become ready" >&2 | |
| sudo docker compose logs --tail=100 bens-db bens-ipfs bens-graph-node | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| BENS_SUBGRAPH_VERSION="github-${DEPLOY_ID}" \ | |
| docker compose --profile bens-deploy run --rm bens-deployer | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose up -d bens | |
| graph_synced=0 | |
| for attempt in $(seq 1 60); do | |
| graph_status="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -fsS \ | |
| -H "Content-Type: application/json" \ | |
| --data-binary '"'"'{"query":"{ _meta { block { number } hasIndexingErrors } }"}'"'"' \ | |
| http://bens-graph-node:8000/subgraphs/name/dos-names 2>/dev/null || true | |
| )" | |
| if jq -e --argjson final_deployment_block "${FINAL_DEPLOYMENT_BLOCK}" \ | |
| '"'"'.data._meta.hasIndexingErrors == false and | |
| (.data._meta.block.number | tonumber) >= $final_deployment_block'"'"' \ | |
| <<<"${graph_status}" >/dev/null 2>&1; then | |
| graph_synced=1 | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${graph_synced}" -ne 1 ]; then | |
| echo "DOS Names subgraph did not index final deployment block ${FINAL_DEPLOYMENT_BLOCK}" >&2 | |
| sudo docker compose logs --tail=150 bens-graph-node | |
| exit 1 | |
| fi | |
| STATELESS_SERVICES="backend frontend smart-contract-verifier visualizer sig-provider user-ops-indexer stats caddy" | |
| for service in ${STATELESS_SERVICES}; do | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" docker compose pull "${service}" | |
| if [ "${service}" = "caddy" ] || | |
| { [ "${service}" = "frontend" ] && [ "${FRONTEND_ENV_CHANGED}" -eq 1 ]; }; then | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose up -d --force-recreate --no-deps "${service}" | |
| else | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose up -d --no-deps "${service}" | |
| fi | |
| done | |
| for attempt in $(seq 1 30); do | |
| rpc_body="{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_chainId\",\"params\":[]}" | |
| if curl -fsS http://127.0.0.1:13080/ >/dev/null && | |
| curl -fsS http://127.0.0.1:13080/api/v2/stats >/dev/null && | |
| curl -fsS http://127.0.0.1:13080/public-metrics >/dev/null && | |
| curl -fsS -H "Content-Type: application/json" --data "${rpc_body}" \ | |
| http://127.0.0.1:8545/ | grep -q "\"result\":\"0xf63\""; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "DOScan Testnet did not become healthy in time" >&2 | |
| sudo docker compose ps | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| local_rpc_response="$( | |
| curl -fsS -D - --max-time 10 \ | |
| -H "Content-Type: application/json" --data "${rpc_body}" \ | |
| http://127.0.0.1:8545/ | |
| )" | |
| grep -q "\"result\":\"0xf63\"" <<<"${local_rpc_response}" | |
| grep -qi "X-DOS-RPC-Origin: dos-testnet-r1-" <<<"${local_rpc_response}" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T frontend node -e '"'"' | |
| const target = process.argv[1]; | |
| const timeout = setTimeout(() => { | |
| console.error("Testnet Caddy WebSocket timed out"); | |
| process.exit(1); | |
| }, 10_000); | |
| const socket = new WebSocket(target); | |
| socket.addEventListener("open", () => { | |
| clearTimeout(timeout); | |
| socket.close(); | |
| console.log("Testnet Caddy WebSocket connected"); | |
| process.exit(0); | |
| }); | |
| socket.addEventListener("error", () => { | |
| clearTimeout(timeout); | |
| console.error("Testnet Caddy WebSocket failed"); | |
| process.exit(1); | |
| }); | |
| '"'"' "ws://caddy:8545/ws" | |
| frontend_env="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T frontend curl -fsS http://localhost:3000/assets/envs.js | |
| )" | |
| grep -Fq "NEXT_PUBLIC_METADATA_SERVICE_API_HOST: \"https://test.doscan.io/metadata-api\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_CONTRACT_INFO_API_HOST: \"https://test.doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${frontend_env}" | |
| verify_frontend_wallets | |
| if grep -F "NEXT_PUBLIC_NETWORK_EXPLORERS:" <<<"${frontend_env}" | grep -Fq "https://test.doscan.io/"; then | |
| echo "Testnet must not link back to itself as an external explorer" >&2 | |
| exit 1 | |
| fi | |
| grep -Fq "NEXT_PUBLIC_HAS_USER_OPS: \"true\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_API_HOST: \"https://test.doscan.io\"" <<<"${frontend_env}" | |
| grep -Pq "NEXT_PUBLIC_NAME_SERVICE_PROTOCOLS: .*\\x27dos-names\\x27" <<<"${frontend_env}" | |
| for attempt in $(seq 1 30); do | |
| if sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -fsS http://bens:8050/health | \ | |
| grep -q '"SERVING"'; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "BENS did not become healthy" >&2 | |
| sudo docker compose logs --tail=100 bens bens-graph-node | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| smoke_lookup_path="/name-service/api/v1/3939/domains/${SMOKE_NAME}" | |
| smoke_lookup_ok=0 | |
| for attempt in $(seq 1 30); do | |
| if smoke_response="$( | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080${smoke_lookup_path}" | |
| )" && | |
| jq -e \ | |
| --arg smoke_name "${SMOKE_NAME}" \ | |
| --arg smoke_address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'.name == $smoke_name and | |
| (.resolved_address.hash | ascii_downcase) == ($smoke_address | ascii_downcase)'"'"' \ | |
| <<<"${smoke_response}" >/dev/null; then | |
| smoke_lookup_ok=1 | |
| break | |
| fi | |
| sleep 5 | |
| done | |
| if [ "${smoke_lookup_ok}" -ne 1 ]; then | |
| echo "Local Caddy/BENS lookup failed for ${SMOKE_NAME}" >&2 | |
| sudo docker compose logs --tail=150 caddy bens bens-graph-node | |
| exit 1 | |
| fi | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/name-service/api/v1/3939/addresses/${SMOKE_RESOLVED_ADDRESS}?protocol_id=dos-names" | \ | |
| jq -e --arg smoke_name "${SMOKE_NAME}" '"'"'.domain.name == $smoke_name'"'"' >/dev/null | |
| curl -fsS -H "Host: test.doscan.io" \ | |
| "http://127.0.0.1:13080/api/v2/search?q=${SMOKE_NAME}" | \ | |
| jq -e \ | |
| --arg smoke_name "${SMOKE_NAME}" \ | |
| --arg smoke_address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'any(.items[]; | |
| .type == "ens_domain" and | |
| (.name // .ens_info.name) == $smoke_name and | |
| (.address_hash | ascii_downcase) == ($smoke_address | ascii_downcase))'"'"' >/dev/null | |
| for attempt in $(seq 1 30); do | |
| if curl -fsS \ | |
| http://127.0.0.1:13080/api/v2/proxy/account-abstraction/status >/dev/null; then | |
| break | |
| fi | |
| if [ "${attempt}" -eq 30 ]; then | |
| echo "Testnet Account Abstraction proxy did not become healthy" >&2 | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose logs --tail=100 user-ops-indexer backend | |
| exit 1 | |
| fi | |
| sleep 5 | |
| done | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -fsS \ | |
| http://smart-contract-verifier:8050/health >/dev/null | |
| # External GCS, Cloudflare, RPC, or metadata failures must not trigger a destructive database restore. | |
| DEPLOYMENT_STARTED=0 | |
| /bin/sh "${SRC}/.github/scripts/verify-testnet-aa-sources.sh" \ | |
| "${SRC}/account-abstraction-verification" | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend curl -fsS \ | |
| "https://metadata.services.blockscout.com/api/v1/metadata?chain_id=3939&addresses=0x0000000000000000000000000000000000000000&tags_limit=1" >/dev/null | |
| verify_nft_media_runtime() { | |
| probe_key="healthchecks/github-${DEPLOY_ID}.txt" | |
| for probe_attempt in $(seq 1 5); do | |
| if probe_output="$( | |
| sudo env DOSCAN_BLOCKSCOUT_SECRETS_ENV="${SECRETS_ENV}" \ | |
| docker compose exec -T backend bin/blockscout rpc \ | |
| "folder = Application.fetch_env!(:nft_media_handler, :r2_folder); key = Path.join(folder, ~s(${probe_key})); body = ~s(doscan-nft-media-runtime); try do true = is_pid(Process.whereis(NFTMediaHandler.Dispatcher)); {:ok, _} = ExAws.S3.put_object(~s(doscan), key, body) |> ExAws.request(); {:ok, %{body: ^body}} = ExAws.S3.get_object(~s(doscan), key) |> ExAws.request(); IO.puts(~s(NFT_MEDIA_RUNTIME_OK)) after ExAws.S3.delete_object(~s(doscan), key) |> ExAws.request() end" | |
| )" && grep -q "NFT_MEDIA_RUNTIME_OK" <<<"${probe_output}"; then | |
| return 0 | |
| fi | |
| sleep 5 | |
| done | |
| echo "NFT media runtime probe failed after 5 attempts" >&2 | |
| return 1 | |
| } | |
| verify_nft_media_runtime | |
| /bin/sh "${SRC}/.github/scripts/retry-public-http.sh" \ | |
| https://test.doscan.io/ >/dev/null | |
| /bin/sh "${SRC}/.github/scripts/retry-public-http.sh" \ | |
| https://test.doscan.io/api/v2/stats >/dev/null | |
| /bin/sh "${SRC}/.github/scripts/retry-public-http.sh" \ | |
| https://test.doscan.io/public-metrics >/dev/null | |
| frontend_env="$( | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://test.doscan.io/assets/envs.js?deploy=${DEPLOY_ID}" | |
| )" | |
| grep -Fq "NEXT_PUBLIC_METADATA_SERVICE_API_HOST: \"https://test.doscan.io/metadata-api\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_CONTRACT_INFO_API_HOST: \"https://test.doscan.io\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_DEX_POOLS_ENABLED: \"true\"" <<<"${frontend_env}" | |
| grep -Fq "NEXT_PUBLIC_NAME_SERVICE_API_HOST: \"https://test.doscan.io\"" <<<"${frontend_env}" | |
| grep -Pq "NEXT_PUBLIC_NAME_SERVICE_PROTOCOLS: .*\\x27dos-names\\x27" <<<"${frontend_env}" | |
| verify_frontend_wallets | |
| if grep -F "NEXT_PUBLIC_NETWORK_EXPLORERS:" <<<"${frontend_env}" | grep -Fq "https://test.doscan.io/"; then | |
| echo "Testnet must not link back to itself as an external explorer" >&2 | |
| exit 1 | |
| fi | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://test.doscan.io/api/v1/chains/3939/pools >/dev/null | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| https://test.doscan.io/name-service/health | grep -q '"SERVING"' | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://test.doscan.io/name-service/api/v1/protocols?chain_id=3939" | \ | |
| grep -q '"dos-names"' | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://test.doscan.io/name-service/api/v1/3939/domains/${SMOKE_NAME}" | \ | |
| jq -e \ | |
| --arg smoke_name "${SMOKE_NAME}" \ | |
| --arg smoke_address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'.name == $smoke_name and | |
| (.resolved_address.hash | ascii_downcase) == ($smoke_address | ascii_downcase)'"'"' >/dev/null | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://test.doscan.io/name-service/api/v1/3939/addresses/${SMOKE_RESOLVED_ADDRESS}?protocol_id=dos-names" | \ | |
| jq -e --arg smoke_name "${SMOKE_NAME}" '"'"'.domain.name == $smoke_name'"'"' >/dev/null | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "https://test.doscan.io/api/v2/search?q=${SMOKE_NAME}" | \ | |
| jq -e \ | |
| --arg smoke_name "${SMOKE_NAME}" \ | |
| --arg smoke_address "${SMOKE_RESOLVED_ADDRESS}" \ | |
| '"'"'any(.items[]; | |
| .type == "ens_domain" and | |
| (.name // .ens_info.name) == $smoke_name and | |
| (.address_hash | ascii_downcase) == ($smoke_address | ascii_downcase))'"'"' >/dev/null | |
| for metadata_url in \ | |
| "https://test.doscan.io/metadata-api/api/v1/metadata" \ | |
| "https://test.doscan.io/api/v1/metadata"; do | |
| metadata_response="$( | |
| curl --fail --silent --show-error --retry 6 --retry-delay 5 \ | |
| "${metadata_url}?addresses=0x9f2aCCb76DEFE1735Fb55c41e1741c54a2d0fEa7&chainId=3939&tagsLimit=1" | |
| )" | |
| grep -q '"addresses"' <<<"${metadata_response}" | |
| done | |
| rpc_response="$( | |
| testnet_rpc_request "${rpc_body}" 1 | |
| )" | |
| grep -q "\"result\":\"0xf63\"" <<<"${rpc_response}" | |
| sudo docker compose ps | |
| echo "GCP testnet deployment completed" | |
| ' | |
| - name: Verify Testnet DOS Name UI with Playwright | |
| env: | |
| DOSCAN_TESTNET_URL: https://test.doscan.io/ | |
| run: | | |
| set -euo pipefail | |
| playwright_dir="$(mktemp -d "${RUNNER_TEMP}/doscan-playwright.XXXXXX")" | |
| trap 'rm -rf "${playwright_dir}"' EXIT | |
| cp .github/scripts/testnet-bens-ui.spec.mjs "${playwright_dir}/" | |
| cd "${playwright_dir}" | |
| npm init --yes >/dev/null | |
| npm install --no-save --package-lock=false @playwright/test@1.55.0 >/dev/null | |
| npx playwright install --with-deps chromium >/dev/null | |
| npx playwright test testnet-bens-ui.spec.mjs --reporter=line | |
| deploy-beta: | |
| if: > | |
| github.ref == 'refs/heads/main' && | |
| github.event_name == 'workflow_dispatch' && | |
| inputs.environment == 'beta' | |
| runs-on: ubuntu-latest | |
| environment: beta | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Validate beta configuration | |
| run: | | |
| set -euo pipefail | |
| secrets_file="$(mktemp)" | |
| trap 'rm -f "${secrets_file}"' EXIT | |
| DOSCAN_BLOCKSCOUT_SECRETS_ENV="${secrets_file}" \ | |
| DOSCAN_BASE_ENV_DIR="${PWD}/docker-compose/envs" \ | |
| docker compose -f docker-compose/docker-compose-beta.yml config -q | |
| docker run --rm \ | |
| -v "${PWD}/docker-compose/Caddyfile-beta:/etc/caddy/Caddyfile:ro" \ | |
| caddy@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 \ | |
| caddy validate --config /etc/caddy/Caddyfile | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| workload_identity_provider: ${{ env.GCP_WORKLOAD_IDENTITY_PROVIDER }} | |
| service_account: ${{ env.GCP_SERVICE_ACCOUNT }} | |
| - name: Set up Google Cloud CLI | |
| uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1 | |
| with: | |
| project_id: ${{ env.GCP_PROJECT_ID }} | |
| - name: Package and upload beta configuration | |
| run: | | |
| set -euo pipefail | |
| archive="/tmp/doscan-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz" | |
| tar -czf "${archive}" \ | |
| docker-compose/docker-compose-beta.yml \ | |
| docker-compose/Caddyfile-beta \ | |
| docker-compose/envs/common-blockscout.env \ | |
| docker-compose/envs/common-blockscout-beta.env \ | |
| docker-compose/envs/common-frontend-beta.env \ | |
| scripts/apply-beta-on-mainnet-vm.sh | |
| gcloud compute scp \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_ZONE}" \ | |
| --tunnel-through-iap \ | |
| "${archive}" \ | |
| "${GCP_INSTANCE}:${archive}" | |
| rm -f "${archive}" | |
| - name: Apply beta configuration and verify services | |
| run: | | |
| set -euo pipefail | |
| gcloud compute ssh "${GCP_INSTANCE}" \ | |
| --project="${GCP_PROJECT_ID}" \ | |
| --zone="${GCP_ZONE}" \ | |
| --tunnel-through-iap \ | |
| --command=" | |
| set -euo pipefail | |
| deploy_id='${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}' | |
| archive='/tmp/doscan-beta-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.tgz' | |
| src=\"\$(mktemp -d /tmp/doscan-beta-deploy.XXXXXX)\" | |
| trap 'rm -rf \"\${src}\" \"\${archive}\"' EXIT | |
| tar -xzf \"\${archive}\" -C \"\${src}\" | |
| sudo bash \"\${src}/scripts/apply-beta-on-mainnet-vm.sh\" \"\${src}\" \"\${deploy_id}\" | |
| curl --fail --silent --show-error --retry 12 --retry-delay 5 \ | |
| https://beta.doscan.io/api/v2/stats >/dev/null | |
| " |