repo: import doublezero-offchain and doublezero-solana with their history #2843
Workflow file for this run
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: shreds-e2e | |
| on: | |
| push: | |
| branches: [main, 'hotfix/**'] | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: Pull request number to test | |
| required: true | |
| head_sha: | |
| description: Expected PR head SHA | |
| required: true | |
| image_tag: | |
| description: GHCR image tag for this trusted run | |
| required: true | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| DZ_SHA: ${{ github.event.inputs.head_sha || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_IMAGE_REPO: ghcr.io/malbeclabs/dz-shreds-e2e | |
| SHRED_IMAGE_TAG: ${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_BASE_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/base:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_LEDGER_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/ledger:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_MANAGER_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/manager:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_ORACLE_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/oracle:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_CLIENT_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/client:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| SHRED_SUBSCRIPTION_API_IMAGE: ghcr.io/malbeclabs/dz-shreds-e2e/subscription-api:${{ github.event.inputs.image_tag || github.event.pull_request.head.sha || github.sha }} | |
| jobs: | |
| setup: | |
| runs-on: self-hosted | |
| timeout-minutes: 20 | |
| permissions: | |
| packages: write | |
| contents: read | |
| checks: write | |
| outputs: | |
| run-e2e: ${{ steps.gate.outputs.run-e2e }} | |
| shreds-sha: ${{ steps.checkout-shreds.outputs.commit }} | |
| matrix: ${{ steps.shard.outputs.matrix }} | |
| steps: | |
| # Version-bump PRs (only version lines change in Cargo.toml/Cargo.lock) | |
| # and docs-only PRs (markdown plus rfcs images) get no signal from e2e. When | |
| # skipping, report the required shard checks as successful on the PR | |
| # head, because the gated matrix job never creates its check runs and | |
| # branch protection would otherwise block the merge. Keep in sync with | |
| # e2e.yml. | |
| - name: Check for docs/version-bump-only PR | |
| id: skip-check | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: actions/github-script@v7 | |
| env: | |
| CHECK_NAME: shard-e2e | |
| # 3 pinned shards + 2 round-robin shards; must match the required | |
| # status check contexts in the main ruleset. | |
| CHECK_SHARDS: "5" | |
| with: | |
| script: | | |
| try { | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.pull_request.number, | |
| per_page: 100, | |
| }); | |
| // Missing patch (diff too large) fails closed: e2e runs. | |
| const versionLinesOnly = (patch) => typeof patch === 'string' && patch | |
| .split('\n') | |
| .filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---)/.test(l)) | |
| .every((l) => /^[+-]version = "/.test(l)); | |
| const isMd = (name) => typeof name === 'string' && name.endsWith('.md'); | |
| // Images under rfcs/ are figures for markdown RFCs; inert for e2e. | |
| const isRfcImage = (name) => typeof name === 'string' && | |
| name.startsWith('rfcs/') && /\.(png|jpe?g|gif|svg|webp)$/i.test(name); | |
| const isInert = (name) => isMd(name) || isRfcImage(name); | |
| // Markdown and rfcs images are inert for e2e; a rename/copy must | |
| // come from an inert path too, since a rename also deletes the | |
| // source path. | |
| const fileOk = (f) => { | |
| if (isInert(f.filename)) { | |
| return (f.status !== 'renamed' && f.status !== 'copied') || isInert(f.previous_filename); | |
| } | |
| return (f.filename === 'Cargo.toml' || f.filename === 'Cargo.lock') && | |
| f.status === 'modified' && | |
| versionLinesOnly(f.patch); | |
| }; | |
| const names = new Set(files.map((f) => f.filename)); | |
| // Cargo.toml and Cargo.lock must change together: a requirement | |
| // change that leaves the lock untouched can still alter what | |
| // unlocked CI builds resolve, and is not a version bump. | |
| const skippable = files.length > 0 && | |
| names.has('Cargo.toml') === names.has('Cargo.lock') && | |
| files.every(fileOk); | |
| core.setOutput('skip', String(skippable)); | |
| if (!skippable) return; | |
| core.notice('Docs/version-bump-only PR; skipping shreds e2e and reporting shard checks as successful.'); | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| for (let shard = 1; shard <= Number(process.env.CHECK_SHARDS); shard++) { | |
| await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: `${process.env.CHECK_NAME} (shard ${shard})`, | |
| head_sha: context.payload.pull_request.head.sha, | |
| status: 'completed', | |
| conclusion: 'success', | |
| details_url: runUrl, | |
| output: { | |
| title: 'Skipped: docs/version-bump-only PR', | |
| summary: 'Only markdown files, rfcs images, and version lines in Cargo.toml/Cargo.lock changed, so shreds e2e was skipped.', | |
| }, | |
| }); | |
| } | |
| } catch (err) { | |
| core.warning(`Skip gate failed; running shreds e2e: ${err}`); | |
| core.setOutput('skip', 'false'); | |
| } | |
| - name: Decide whether privileged shreds e2e can run | |
| id: gate | |
| run: | | |
| if [ "${{ steps.skip-check.outputs.skip }}" = "true" ]; then | |
| echo "run-e2e=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Skipping shreds e2e: docs/version-bump-only PR." | |
| exit 0 | |
| fi | |
| if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then | |
| echo "run-e2e=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Skipping privileged shreds e2e on an external fork PR. A maintainer can comment /run-e2e to start a trusted run." | |
| { | |
| echo "### Privileged shreds e2e skipped" | |
| echo | |
| echo "This pull request comes from an external fork, so GitHub withholds repository secrets." | |
| echo "A maintainer can comment \`/run-e2e\` to dispatch trusted shreds e2e for this PR head SHA." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 0 | |
| fi | |
| echo "run-e2e=true" >> "$GITHUB_OUTPUT" | |
| - name: Checkout doublezero-shreds | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| id: checkout-shreds | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: malbeclabs/doublezero-shreds | |
| token: ${{ secrets.MALBEC_INFRA_RO }} | |
| - uses: actions/setup-go@v5 | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Login to GHCR | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Override doublezero SHA | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| run: | | |
| echo "Using doublezero SHA: $DZ_SHA" | |
| jq --arg sha "$DZ_SHA" '.doublezero = $sha' e2e/.dep-shas.json > e2e/.dep-shas.json.tmp | |
| mv e2e/.dep-shas.json.tmp e2e/.dep-shas.json | |
| cat e2e/.dep-shas.json | |
| - name: Build images | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| working-directory: e2e/ | |
| run: go run -tags=e2e ./cmd/devnet build -v | |
| - name: Push images to registry | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| run: | | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/base:${{ env.SHRED_IMAGE_TAG }} | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/ledger:${{ env.SHRED_IMAGE_TAG }} | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/manager:${{ env.SHRED_IMAGE_TAG }} | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/oracle:${{ env.SHRED_IMAGE_TAG }} | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/client:${{ env.SHRED_IMAGE_TAG }} | |
| docker push ${{ env.SHRED_IMAGE_REPO }}/subscription-api:${{ env.SHRED_IMAGE_TAG }} | |
| - name: Discover tests and distribute across shards | |
| if: steps.gate.outputs.run-e2e == 'true' | |
| id: shard | |
| working-directory: e2e/ | |
| run: | | |
| # Find all TestE2E_* functions in files with the e2e build tag. | |
| tests=$(grep -rl '^//go:build e2e$' ./*_test.go \ | |
| | xargs grep -h '^func TestE2E_' \ | |
| | sed 's/func \(TestE2E_[a-zA-Z0-9_]*\).*/\1/' \ | |
| | sort) | |
| count=$(echo "$tests" | wc -l) | |
| echo "Discovered $count tests" | |
| echo "$tests" | |
| # Pinned heavy tests: each gets its own shard slot to keep the | |
| # biggest rocks off the same runner. Update this list if a new test | |
| # exceeds ~5min, or if a pinned test is renamed/removed upstream | |
| # (the validation below will fail fast in that case). | |
| pinned_1="TestE2E_MultiUserInstantAllocationAndWithdrawal" | |
| pinned_2="TestE2E_DeviceScale" | |
| # ~9min, over half of it a single 40-day ledger warp (three validator | |
| # restart hops). On a round-robin shard it only passed when Go's | |
| # parallel scheduler happened to start it early. | |
| pinned_3="TestE2E_FeedSubscriptionOracleExpiryTeardown" | |
| # Fail fast if a pinned test no longer exists in the shreds repo — | |
| # otherwise its shard would silently run zero tests. | |
| for pin in "$pinned_1" "$pinned_2" "$pinned_3"; do | |
| if ! echo "$tests" | grep -qxF "$pin"; then | |
| echo "::error::Pinned test '$pin' not found in doublezero-shreds. Update the pin list in this workflow." | |
| exit 1 | |
| fi | |
| done | |
| # Remaining tests round-robin across shards 4 and 5. | |
| remaining=$(echo "$tests" | grep -vE "^(${pinned_1}|${pinned_2}|${pinned_3})$") | |
| ROUND_ROBIN_SHARDS=2 | |
| declare -a shards | |
| for ((i=0; i<ROUND_ROBIN_SHARDS; i++)); do shards[i]=""; done | |
| i=0 | |
| while IFS= read -r test; do | |
| idx=$((i % ROUND_ROBIN_SHARDS)) | |
| if [ -n "${shards[idx]}" ]; then | |
| shards[idx]="${shards[idx]}|${test}" | |
| else | |
| shards[idx]="$test" | |
| fi | |
| i=$((i + 1)) | |
| done <<< "$remaining" | |
| # Build JSON matrix: shards 1-3 are pinned, shards 4-5 are round-robin. | |
| matrix="[{\"shard\":1,\"run\":\"^(${pinned_1})$\"}" | |
| matrix="${matrix},{\"shard\":2,\"run\":\"^(${pinned_2})$\"}" | |
| matrix="${matrix},{\"shard\":3,\"run\":\"^(${pinned_3})$\"}" | |
| for ((i=0; i<ROUND_ROBIN_SHARDS; i++)); do | |
| matrix="${matrix},{\"shard\":$((i + 4)),\"run\":\"^(${shards[i]})$\"}" | |
| done | |
| matrix="${matrix}]" | |
| echo "Matrix: $matrix" | |
| echo "matrix=$matrix" >> "$GITHUB_OUTPUT" | |
| shard-e2e: | |
| name: shard-e2e (shard ${{ matrix.shard }}) | |
| needs: setup | |
| if: needs.setup.outputs.run-e2e == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: ${{ fromJSON(needs.setup.outputs.matrix) }} | |
| runs-on: doublezero-k8s-ci | |
| timeout-minutes: 15 | |
| permissions: | |
| packages: read | |
| contents: read | |
| checks: write | |
| defaults: | |
| run: | |
| working-directory: e2e/ | |
| steps: | |
| # Trusted (workflow_dispatch) runs execute on the base ref, so their native | |
| # check runs attach to that commit, not the PR head. Report a check run on | |
| # the PR head SHA so branch protection's required context is met. | |
| - name: Report check-run start on PR head | |
| if: github.event_name == 'workflow_dispatch' | |
| id: check | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data } = await github.rest.checks.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: 'shard-e2e (shard ${{ matrix.shard }})', | |
| head_sha: context.payload.inputs.head_sha, | |
| status: 'in_progress', | |
| details_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); | |
| core.setOutput('id', String(data.id)); | |
| - name: Checkout doublezero-shreds | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: malbeclabs/doublezero-shreds | |
| ref: ${{ needs.setup.outputs.shreds-sha }} | |
| token: ${{ secrets.MALBEC_INFRA_RO }} | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Login to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Pull pre-built images | |
| run: | | |
| pull_with_retry() { | |
| local image=$1 | |
| local max_attempts=3 | |
| local attempt=1 | |
| while [ $attempt -le $max_attempts ]; do | |
| echo "Pulling $image (attempt $attempt/$max_attempts)" | |
| if docker pull "$image"; then | |
| return 0 | |
| fi | |
| echo "Pull failed, retrying in 5 seconds..." | |
| sleep 5 | |
| attempt=$((attempt + 1)) | |
| done | |
| echo "Failed to pull $image after $max_attempts attempts" | |
| return 1 | |
| } | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/base:${{ env.SHRED_IMAGE_TAG }} | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/ledger:${{ env.SHRED_IMAGE_TAG }} | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/manager:${{ env.SHRED_IMAGE_TAG }} | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/oracle:${{ env.SHRED_IMAGE_TAG }} | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/client:${{ env.SHRED_IMAGE_TAG }} | |
| pull_with_retry ${{ env.SHRED_IMAGE_REPO }}/subscription-api:${{ env.SHRED_IMAGE_TAG }} | |
| - name: Run e2e tests | |
| id: test | |
| env: | |
| SHRED_E2E_NO_BUILD: "1" | |
| run: go test -tags=e2e -timeout=12m -count=1 -run '${{ matrix.run }}' -v . | |
| - name: Report check-run result on PR head | |
| if: always() && github.event_name == 'workflow_dispatch' && steps.check.outputs.id | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const outcome = '${{ steps.test.outcome }}'; | |
| const conclusion = outcome === 'success' ? 'success' : (outcome === 'cancelled' ? 'cancelled' : 'failure'); | |
| await github.rest.checks.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| check_run_id: Number('${{ steps.check.outputs.id }}'), | |
| status: 'completed', | |
| conclusion, | |
| details_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, | |
| }); |