diff --git a/.github/actions/install-anvil/action.yml b/.github/actions/install-anvil/action.yml new file mode 100644 index 000000000000..f6e867019c8e --- /dev/null +++ b/.github/actions/install-anvil/action.yml @@ -0,0 +1,159 @@ +name: Install Anvil +description: >- + Install Anvil directly, using a checksum-verified Actions cache only when + the direct download fails. + +inputs: + maintain-fallback-cache: + description: >- + Check and populate the fallback cache without installing Anvil for use. + This is intended for one trusted main-branch job. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Resolve Anvil cache metadata + id: cache-metadata + shell: bash + run: | + node <<'NODE' + const { appendFileSync } = require('node:fs'); + + const packageJson = require('./package.json'); + const foundryupPackage = require('@metamask/foundryup/package.json'); + const architecture = { arm64: 'arm64', x64: 'amd64' }[process.arch]; + + if (!architecture) { + throw new Error(`Unsupported architecture: ${process.arch}`); + } + + const target = `${process.platform}-${architecture}`; + const foundryConfig = packageJson.foundryup; + const checksum = foundryConfig.checksums.binaries.anvil[target]; + + if (!checksum) { + throw new Error(`Missing Anvil checksum for ${target}`); + } + + const cacheKey = [ + 'anvil-cache-v1', + process.platform, + architecture, + `foundryup-${foundryupPackage.version}`, + `foundry-${foundryConfig.version}`, + checksum, + ].join('-'); + + appendFileSync( + process.env.GITHUB_OUTPUT, + `cache-key=${cacheKey}\nchecksum=${checksum}\n`, + ); + NODE + + - name: Check for maintained Anvil fallback cache + id: maintained-cache + if: inputs.maintain-fallback-cache == 'true' + continue-on-error: true + uses: actions/cache/restore@v5 + with: + path: .metamask/cache + key: ${{ steps.cache-metadata.outputs.cache-key }} + lookup-only: true + + - name: Populate Anvil fallback cache + id: populate-cache + if: >- + inputs.maintain-fallback-cache == 'true' && + steps.maintained-cache.outputs.cache-hit != 'true' + continue-on-error: true + shell: bash + run: timeout --kill-after=10s 60s yarn mm-foundryup + + - name: Save Anvil fallback cache + if: >- + inputs.maintain-fallback-cache == 'true' && + steps.maintained-cache.outputs.cache-hit != 'true' && + steps.populate-cache.outcome == 'success' + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: .metamask/cache + key: ${{ steps.cache-metadata.outputs.cache-key }} + + - name: Install Anvil directly + id: direct-install + if: inputs.maintain-fallback-cache != 'true' + continue-on-error: true + shell: bash + run: timeout --kill-after=10s 60s yarn mm-foundryup + + - name: Clean up failed direct download + if: >- + inputs.maintain-fallback-cache != 'true' && + steps.direct-install.outcome == 'failure' + continue-on-error: true + shell: bash + run: yarn mm-foundryup cache clean + + - name: Restore Anvil fallback cache + id: fallback-cache + if: >- + inputs.maintain-fallback-cache != 'true' && + steps.direct-install.outcome == 'failure' + continue-on-error: true + uses: actions/cache/restore@v5 + with: + path: .metamask/cache + key: ${{ steps.cache-metadata.outputs.cache-key }} + + - name: Verify cached Anvil checksum + id: verify-cache + if: steps.fallback-cache.outputs.cache-hit == 'true' + continue-on-error: true + shell: bash + env: + EXPECTED_CHECKSUM: ${{ steps.cache-metadata.outputs.checksum }} + run: | + mapfile -d '' -t cached_files < <( + find .metamask/cache -mindepth 2 -maxdepth 2 -type f -print0 + ) + + if [[ ${#cached_files[@]} -ne 1 || ${cached_files[0]##*/} != 'anvil' ]]; then + echo '::warning::The Anvil fallback cache has an unexpected layout.' + exit 1 + fi + + if ! echo "${EXPECTED_CHECKSUM} ${cached_files[0]}" | sha256sum --check --status; then + echo '::warning::The cached Anvil binary failed checksum verification.' + exit 1 + fi + + - name: Install Anvil from fallback cache + id: cached-install + if: steps.verify-cache.outcome == 'success' + continue-on-error: true + shell: bash + run: yarn mm-foundryup + + - name: Clean up unusable fallback cache + if: >- + inputs.maintain-fallback-cache != 'true' && + steps.direct-install.outcome == 'failure' && + steps.cached-install.outcome != 'success' + continue-on-error: true + shell: bash + run: yarn mm-foundryup cache clean + + - name: Retry direct Anvil installation + if: >- + inputs.maintain-fallback-cache != 'true' && + steps.direct-install.outcome == 'failure' && + steps.cached-install.outcome != 'success' + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 + with: + timeout_minutes: 1 + max_attempts: 4 + retry_wait_seconds: 30 + command: yarn mm-foundryup cache clean && yarn mm-foundryup diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 48a3a684f078..f84443f615c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -83,6 +83,14 @@ jobs: cache-node-modules: true skip-allow-scripts: true + # Keep a single checksum-verified fallback cache in the default branch's + # cache scope. Consumer jobs restore it only after a direct download fails. + - name: Maintain Anvil fallback cache + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'MetaMask/metamask-extension' }} + uses: ./.github/actions/install-anvil + with: + maintain-fallback-cache: true + lint-workflows: name: Lint workflows if: ${{ needs.get-requirements.outputs.skip-everything != 'true' }} diff --git a/.github/workflows/run-benchmarks.yml b/.github/workflows/run-benchmarks.yml index 6dc5e06d1b5c..1f7aa4dc9cde 100644 --- a/.github/workflows/run-benchmarks.yml +++ b/.github/workflows/run-benchmarks.yml @@ -57,13 +57,8 @@ jobs: is-high-risk-environment: false skip-allow-scripts: true - - name: Install anvil with retry - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 - with: - timeout_minutes: 1 - max_attempts: 4 - retry_wait_seconds: 30 - command: yarn mm-foundryup + - name: Install Anvil + uses: ./.github/actions/install-anvil - name: Download build artifact uses: actions/download-artifact@v7 diff --git a/.github/workflows/run-e2e.yml b/.github/workflows/run-e2e.yml index 186305353540..8b959a39b820 100644 --- a/.github/workflows/run-e2e.yml +++ b/.github/workflows/run-e2e.yml @@ -91,13 +91,8 @@ jobs: is-high-risk-environment: false skip-allow-scripts: true - - name: Install anvil with retry - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 - with: - timeout_minutes: 1 - max_attempts: 4 - retry_wait_seconds: 30 - command: yarn mm-foundryup + - name: Install Anvil + uses: ./.github/actions/install-anvil - name: Download build artifact if: ${{ inputs.build-artifact != '' }} diff --git a/.github/workflows/update-e2e-fixtures.yml b/.github/workflows/update-e2e-fixtures.yml index f6f93f9894c6..2c7ed0f915ef 100644 --- a/.github/workflows/update-e2e-fixtures.yml +++ b/.github/workflows/update-e2e-fixtures.yml @@ -180,13 +180,8 @@ jobs: is-high-risk-environment: false skip-allow-scripts: true - - name: Install anvil with retry - uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 - with: - timeout_minutes: 1 - max_attempts: 4 - retry_wait_seconds: 30 - command: yarn mm-foundryup + - name: Install Anvil + uses: ./.github/actions/install-anvil - name: Download dist artifact uses: actions/download-artifact@v7