diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7ffb8e3f..c83a2c36 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,35 +1,36 @@ # Source-of-truth E2E pipeline workflow. # -# This file is BOTH: -# - The workflow that runs in this repo (genlayer-e2e) for dispatch / -# local testing. -# - The file synced verbatim to every consumer repo as -# .github/workflows/e2e.yml (sync-template.yaml owns the fan-out). +# This file is the central genlayer-e2e entrypoint. The primary PR gate +# path is GitHub App driven: the webhook service creates/updates the +# source repo's `E2E Tests` check and dispatches this workflow directly +# in genlayer-e2e. Consumer repos do not need a synced workflow for that +# App path. # -# Consumers don't carry a separate thin caller — keeping the consumer's -# workflow file BYTE-IDENTICAL to this one means: -# - Sidebar tiles stay flat: `acknowledge / register`, `plan / action`, -# `build / discover`, `genlayer-core / shard ... / e2e`, `result` — -# no wrapper-job prefix. A consumer-side thin caller would force -# ` /` on every inner tile (see #386 follow-up where -# deleting e2e-harness.yml restored the flat layout). -# - Behavior changes ship via one sync PR per consumer, instead of -# each consumer hand-editing their wrapper. +# The historical synced consumer workflow path is still supported as a +# legacy/manual fallback while repositories migrate to the App gate. # # Two trigger paths: # - `issue_comment` — PR comment `/run-e2e [profile] [track] [scope]` # on the consumer's repo. The acknowledge job's `if:` gates on # author-association so only members/owners/collaborators can fire # the pipeline. -# - `workflow_dispatch` — manual / debug in this repo (UI dropdowns). -# Acknowledge is skipped (no PR comment context); plan/build/waves -# run with workflow_dispatch input values. +# - `workflow_dispatch` with `dispatch_payload` — primary GitHub App +# dispatcher path. The webhook service validates the public/private +# policy, creates the `E2E Tests` check on the source PR, then +# dispatches this central private run with synthetic PR context. +# - `repository_dispatch` — legacy GitHub App dispatcher path. The webhook +# service validates the public/private policy, creates the `E2E Tests` +# check on the source PR, then dispatches this central private run with +# synthetic PR context. +# - `workflow_dispatch` without `dispatch_payload` — manual / debug in +# this repo (UI dropdowns). Acknowledge is skipped (no PR comment +# context); plan/build/waves run with workflow_dispatch input values. # -# All internal `uses:` references are cross-repo -# (`genlayerlabs/genlayer-e2e/.github/workflows/X.yml@main`) so the same -# file works whether it lives in this repo or in a synced consumer. -# Feature-branch testing requires sedding `@main` → `@` on the -# inner uses; see feedback_branch_pin_for_testing.md. +# Internal reusable workflow calls use local paths so the GitHub App / +# workflow_dispatch path validates and runs against this exact +# genlayer-e2e commit. The legacy sync renderer rewrites those local +# paths to `genlayerlabs/genlayer-e2e/.github/workflows/X.yml@main` only +# for consumer copies. # # Sync-time hack — `on: issue_comment:` injection # ------------------------------------------------ @@ -70,6 +71,15 @@ run-name: >- ${{ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/run-e2e') && format('PR #{0} /run-e2e', github.event.issue.number)) + || (github.event_name == 'repository_dispatch' + && format('{0} #{1} /run-e2e', + github.event.client_payload.dispatch.target_repo || github.event.client_payload.target_repo, + github.event.client_payload.dispatch.pr_number || github.event.client_payload.pr_number)) + || (github.event_name == 'workflow_dispatch' + && inputs.dispatch_payload != '' + && format('{0} #{1} /run-e2e', + inputs.target_repo, + inputs.pr_number)) || (github.event_name == 'workflow_dispatch' && format('E2E Test {0}/{1}/{2}/{3}', inputs.profile, inputs.track, inputs.scope, inputs.stack)) || format('noop {0}', github.run_id) }} @@ -77,6 +87,8 @@ run-name: >- on: issue_comment: types: [created] + repository_dispatch: + types: [e2e-dispatch] workflow_dispatch: inputs: profile: @@ -88,12 +100,13 @@ on: - default - testnet track: - description: Matrix track (must exist as matrix/.yaml) + description: E2E release train (must exist in release-trains.yaml) required: false default: v0.5 type: choice options: - v0.5 + - v0.6 scope: description: > Wave-plan scope filter. `all` runs every wave (default); @@ -121,6 +134,29 @@ on: - all - dev-env - studio + dispatch_payload: + description: GitHub App dispatch payload JSON. Internal; leave empty for manual debug runs. + required: false + default: '' + type: string + target_repo: + description: Source repo for App dispatch display/grouping. Internal. + required: false + default: '' + type: string + pr_number: + description: Source PR number for App dispatch display/grouping. Internal. + required: false + default: '' + type: string + combo-cache-bust: + description: > + Optional component-combo cache bust token for debug runs. + Use `force` / `no-cache` for a one-off miss, or a stable token + to invalidate a family of cached successes. + required: false + default: '' + type: string permissions: contents: read @@ -139,6 +175,15 @@ concurrency: ${{ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/run-e2e')) && format('e2e-{0}-pr-{1}', github.repository, github.event.issue.number) + || (github.event_name == 'repository_dispatch' + && format('e2e-{0}-pr-{1}', + github.event.client_payload.dispatch.target_repo || github.event.client_payload.target_repo, + github.event.client_payload.dispatch.pr_number || github.event.client_payload.pr_number)) + || (github.event_name == 'workflow_dispatch' + && inputs.dispatch_payload != '' + && format('e2e-{0}-pr-{1}', + inputs.target_repo, + inputs.pr_number)) || format('e2e-noop-{0}', github.run_id) }} cancel-in-progress: true @@ -153,6 +198,104 @@ env: E2E_REPORT_SKIP_RESULT: 'true' jobs: + # =========================================================================== + # dispatch-context — central GitHub App webhook path. + # + # The dispatcher service has already verified the webhook, actor, trusted-head + # policy, and source PR metadata before dispatching this workflow. This job + # validates the payload contract again inside GitHub Actions, updates the + # already-created source check-run with this private run URL, parses the + # comment tokens, parses Depends-On from the forwarded PR body, and emits the + # same context fields that acknowledge emits on the synced issue_comment path. + # =========================================================================== + dispatch-context: + if: github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.dispatch_payload != '') + runs-on: ubuntu-latest + outputs: + target-repo: ${{ steps.payload.outputs.target_repo }} + pr-number: ${{ steps.payload.outputs.pr_number }} + comment-id: ${{ steps.payload.outputs.comment_id }} + check-run-id: ${{ steps.payload.outputs.check_run_id }} + head-sha: ${{ steps.payload.outputs.head_sha }} + public-result-policy: ${{ steps.payload.outputs.public_result_policy }} + profile-token: ${{ steps.parse.outputs.profile-token }} + track-token: ${{ steps.parse.outputs.track-token }} + scope-token: ${{ steps.parse.outputs.scope-token }} + stack-token: ${{ steps.parse.outputs.stack-token }} + combo-cache-bust: ${{ steps.payload.outputs.combo_cache_bust || steps.parse.outputs.combo-cache-bust-token }} + pre-build-cache: ${{ steps.pre-build-cache.outputs.pre-build-cache }} + depends-on-tsv: ${{ steps.validate-depends-on.outputs.depends-on-tsv }} + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: '22' + + - name: Validate dispatcher payload + id: payload + env: + PAYLOAD: ${{ github.event_name == 'repository_dispatch' && toJson(github.event.client_payload.dispatch || github.event.client_payload) || inputs.dispatch_payload }} + run: bash services/e2e-dispatcher/scripts/emit-dispatch-payload-outputs.sh + + - name: Generate GitHub App token + id: app-token + uses: ./.github/actions/gcp-app-token + + - name: Mark source check in progress + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + TARGET_REPO: ${{ steps.payload.outputs.target_repo }} + COMMENT_ID: ${{ steps.payload.outputs.comment_id }} + CHECK_RUN_ID: ${{ steps.payload.outputs.check_run_id }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: ./taskfiles/runner/scripts/mark-dispatch-check-in-progress.sh + + - name: Parse trigger comment + id: parse + uses: ./.github/actions/parse-comment + with: + comment-body: ${{ steps.payload.outputs.comment_body }} + + - name: Validate Depends-On lines + id: validate-depends-on + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_BODY: ${{ steps.payload.outputs.pull_request_body }} + COMPONENTS_FILE: ${{ github.workspace }}/components.yaml + run: | + set -euo pipefail + tsv=$(./taskfiles/runner/scripts/parse-depends-on.sh) + { + echo "depends-on-tsv<> "$GITHUB_OUTPUT" + + - name: Decide pre-build-cache cleavage + id: pre-build-cache + env: + TARGET_REPO: ${{ steps.payload.outputs.target_repo }} + run: | + case "${TARGET_REPO}" in + genlayerlabs/genlayer-node) cleavage="configure_node" ;; + *) cleavage="" ;; + esac + echo "pre-build-cache=${cleavage}" >> "$GITHUB_OUTPUT" + + - name: Notify dispatch context failure + if: failure() && steps.payload.outputs.check_run_id != '' + uses: ./.github/actions/notify-outcome + with: + outcome: failure + check-run-title: 'E2E Tests — Setup Failed' + repo: ${{ steps.payload.outputs.target_repo }} + comment-id: ${{ steps.payload.outputs.comment_id }} + check-run-id: ${{ steps.payload.outputs.check_run_id }} + pr-number: ${{ steps.payload.outputs.pr_number }} + github-token: ${{ steps.app-token.outputs.token }} + public-result-policy: ${{ steps.payload.outputs.public_result_policy }} + # =========================================================================== # acknowledge — PR-side bookkeeping and comment tokenization. # @@ -185,6 +328,14 @@ jobs: target-repo: ${{ github.repository }} server-url: ${{ github.server_url }} run-id: ${{ github.run_id }} + # template-hash is the sha256 of this file's SOURCE on + # genlayer-e2e (pre-injection); sync-templates.sh fills it in + # for each consumer at sync time. The acknowledge job compares + # it to the current source hash and fails fast when the consumer + # is behind on syncs. The empty literal here is what the source + # ships with — it stays empty in the genlayer-e2e repo itself + # (skipped at the check step on the source-side path). + template-hash: '0bea0e74a739169d8dedf19fb3b4300aed42dadd43a806438a3fae0ebe8f8f87' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== @@ -197,34 +348,42 @@ jobs: # the `github.event_name == 'workflow_dispatch'` clause filters it out. # =========================================================================== plan: - needs: acknowledge + needs: [acknowledge, dispatch-context] if: | !cancelled() && (needs.acknowledge.result == 'success' || + needs.dispatch-context.result == 'success' || (needs.acknowledge.result == 'skipped' && github.event_name == 'workflow_dispatch')) uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-planner.yml@main with: # Coalesce: acknowledge tokens win on the PR-comment path; on # workflow_dispatch the tokens are empty and we fall back to # the manual choice inputs. - profile: ${{ needs.acknowledge.outputs.profile-token || inputs.profile }} - track: ${{ needs.acknowledge.outputs.track-token || inputs.track }} - scope: ${{ needs.acknowledge.outputs.scope-token || inputs.scope }} + profile: ${{ needs.acknowledge.outputs.profile-token || needs.dispatch-context.outputs.profile-token || inputs.profile }} + track: ${{ needs.acknowledge.outputs.track-token || needs.dispatch-context.outputs.track-token || inputs.track }} + scope: ${{ needs.acknowledge.outputs.scope-token || needs.dispatch-context.outputs.scope-token || inputs.scope }} # SYNC_INJECT(default_stack_fallback) rewrites the literal 'all' on # consumer copies (e.g. 'dev-env' for genlayer-node), so an # issue_comment `/run-e2e` with no stack token still routes to # the per-consumer default — inputs.stack is workflow_dispatch- # only and resolves empty on the issue_comment path. - stack: ${{ needs.acknowledge.outputs.stack-token || inputs.stack || 'all' }} # SYNC_INJECT(default_stack_fallback) - target-repo: ${{ github.repository }} - pr-number: ${{ github.event.issue.number || '' }} - comment-id: ${{ github.event.comment.id || '' }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} + stack: ${{ needs.acknowledge.outputs.stack-token || needs.dispatch-context.outputs.stack-token || inputs.stack || 'all' }} # SYNC_INJECT(default_stack_fallback) + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || '' }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} # acknowledge picks the layered-cache cleavage based on the # consumer repo. On workflow_dispatch acknowledge is skipped # and its output is empty — the planner's empty default then # disables layering (today's pre-Slice-A behaviour). - pre-build-cache: ${{ needs.acknowledge.outputs.pre-build-cache || '' }} + pre-build-cache: ${{ needs.acknowledge.outputs.pre-build-cache || needs.dispatch-context.outputs.pre-build-cache || '' }} + # Pre-parsed Depends-On TSV (one `\t` line per entry). + # Produced by acknowledge's validate-depends-on step. Empty on + # workflow_dispatch (no PR body to parse) — plan's resolve-matrix / + # resolve-components treat empty as "no overrides", same as the + # legacy path that used to do its own parse. + depends-on-tsv: ${{ needs.acknowledge.outputs.depends-on-tsv || needs.dispatch-context.outputs.depends-on-tsv || '' }} # =========================================================================== # build — full stack up + pack to cache. Synthetic PR context on the @@ -233,7 +392,7 @@ jobs: # =========================================================================== build: name: build (dev-env) - needs: [acknowledge, plan] + needs: [acknowledge, dispatch-context, plan] # Without an explicit `if:`, GHA's implicit `success()` would require # acknowledge to have succeeded — but acknowledge is intentionally # skipped on the workflow_dispatch path. Mirror the wave jobs and @@ -249,7 +408,7 @@ jobs: contains(needs.plan.outputs.stack-config, '"target":"dev-env"') uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-build.yml@main with: - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} profile: ${{ needs.plan.outputs.profile }} genvm-version: ${{ needs.plan.outputs.genvm-version }} consensus-ref: ${{ needs.plan.outputs.consensus-ref }} @@ -263,18 +422,18 @@ jobs: build-cache-key: ${{ needs.plan.outputs.build-cache-key }} full-cache-key: ${{ needs.plan.outputs.full-cache-key }} pre-build-cache: ${{ needs.plan.outputs.pre-build-cache }} - # Mirrors matrix/.yaml shape ({core, harness, tooling}). The + # Mirrors the resolved track matrix shape ({core, harness, tooling}). The # conclusion job's Emit build summary step parses this with jq # to render the full component Refs sub-list in the Execution # block. Trailing JSON commas would be invalid — keep the same # field set as the matrix file. matrix-json: >- - {"core":{"genlayer-node":"${{ needs.plan.outputs.genlayer-node-ref }}","genlayer-consensus":"${{ needs.plan.outputs.consensus-ref }}","genvm":"${{ needs.plan.outputs.genvm-version }}"},"harness":{"genlayer-dev-env":"${{ needs.plan.outputs.harness-ref }}"},"tooling":{"genlayer-js":"${{ needs.plan.outputs.genlayer-js-ref }}","genlayer-py":"${{ needs.plan.outputs.genlayer-py-ref }}","genlayer-cli":"${{ needs.plan.outputs.genlayer-cli-ref }}","genlayer-studio":"${{ needs.plan.outputs.genlayer-studio-ref }}","genlayer-explorer":"${{ needs.plan.outputs.genlayer-explorer-ref }}","genlayer-testing-suite":"${{ needs.plan.outputs.genlayer-testing-suite-ref }}","genvm-linter":"${{ needs.plan.outputs.genvm-linter-ref }}","genlayer-wallet":"${{ needs.plan.outputs.genlayer-wallet-ref }}"}} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + {"core":{"genlayer-node":"${{ needs.plan.outputs.genlayer-node-ref }}","genlayer-consensus":"${{ needs.plan.outputs.consensus-ref }}","genvm":"${{ needs.plan.outputs.genvm-version }}"},"harness":{"genlayer-dev-env":"${{ needs.plan.outputs.harness-ref }}"},"tooling":{"genlayer-js":"${{ needs.plan.outputs.genlayer-js-ref }}","genlayer-py":"${{ needs.plan.outputs.genlayer-py-ref }}","genlayer-cli":"${{ needs.plan.outputs.genlayer-cli-ref }}","genlayer-studio":"${{ needs.plan.outputs.genlayer-studio-ref }}","genlayer-explorer":"${{ needs.plan.outputs.genlayer-explorer-ref }}","genlayer-testing-suite":"${{ needs.plan.outputs.genlayer-testing-suite-ref }}","genvm-linter":"${{ needs.plan.outputs.genvm-linter-ref }}","genlayer-wallet":"${{ needs.plan.outputs.genlayer-wallet-ref }}","genlayer-project-boilerplate":"${{ needs.plan.outputs.genlayer-project-boilerplate-ref }}","vscode-extension":"${{ needs.plan.outputs.vscode-extension-ref }}"}} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} github-retry-max: ${{ needs.plan.outputs.github-retry-max }} github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} @@ -287,23 +446,23 @@ jobs: # =========================================================================== build-studio: name: build (studio) - needs: [acknowledge, plan] + needs: [acknowledge, dispatch-context, plan] if: | !cancelled() && needs.plan.result == 'success' && contains(needs.plan.outputs.stack-config, '"target":"studio"') uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-build-studio.yml@main with: - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} profile: ${{ needs.plan.outputs.profile }} genvm-version: ${{ needs.plan.outputs.genvm-version }} genlayer-studio-ref: ${{ needs.plan.outputs.genlayer-studio-ref }} studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} # =========================================================================== # Wave jobs — cascade pattern with sentinel-as-skip. Each wave matrix @@ -327,7 +486,7 @@ jobs: # reading `needs.wave-K.outputs.failure-label` (the per-component # layer tag emitted by e2e-run.yml when that wave's run failed). name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio] + needs: [acknowledge, dispatch-context, plan, build, build-studio] # No `build-status == 'success'` gate — when build fails we want # wave-1 to RUN (so the `name:` expression evaluates and tiles # render cleanly) but no-op via the sentinel-component override @@ -364,7 +523,7 @@ jobs: # mechanism waves 2-4 use for upstream-wave failures — see # e2e-run.yml's sentinel branches. component: ${{ ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) && 'none' || matrix.component }} - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} job-name: ${{ matrix.job-name || matrix.component }} stack-target: ${{ matrix.stack-target || 'dev-env' }} setup-task: ${{ matrix.setup-task }} @@ -372,6 +531,7 @@ jobs: tags: ${{ matrix.tags }} split: ${{ matrix.split }} features-source: ${{ matrix.features-source }} + e2e-dir: ${{ needs.plan.outputs.e2e-dir }} retry: ${{ matrix.retry }} max-shard-split: ${{ matrix.max-shard-split || 0 }} failure-tag: ${{ matrix.failure-tag || '' }} @@ -386,16 +546,20 @@ jobs: genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} + genlayer-project-boilerplate-ref: ${{ needs.plan.outputs.genlayer-project-boilerplate-ref }} + vscode-extension-ref: ${{ needs.plan.outputs.vscode-extension-ref }} harness-ref: ${{ needs.plan.outputs.harness-ref }} harness-sha: ${{ needs.plan.outputs.harness-sha }} cache-key: ${{ needs.plan.outputs.full-cache-key }} studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} + combo-cache-bust: ${{ needs.acknowledge.outputs.combo-cache-bust-token || needs.dispatch-context.outputs.combo-cache-bust || inputs.combo-cache-bust || '' }} github-retry-max: ${{ needs.plan.outputs.github-retry-max }} github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} @@ -414,7 +578,7 @@ jobs: # 'failure'. Empty otherwise — so a successful or sentinel-skipped # wave doesn't carry a stale tag forward. name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-1.result == 'failure') && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1] + needs: [acknowledge, dispatch-context, plan, build, build-studio, wave-1] # No cascade gate — wave-2 always runs when plan is OK. If # build or wave-1 failed, with.component is overridden to 'none' # below, tripping e2e-run.yml's sentinel path (allocate/shard/ @@ -424,7 +588,14 @@ jobs: !cancelled() && needs.plan.result == 'success' strategy: - fail-fast: false + fail-fast: true + # Wave-2 SDK components can each fan out into multiple stack shards. + # Letting JS/Python/CLI all run at once multiplies Studio stack + # count (5 shards * 3 components = 15 stacks) and makes Studio + # transactions stall. Run one SDK component at a time and stop the + # rest of this wave on the first failed component; each component + # can still use its internal shard split. + max-parallel: 1 matrix: include: ${{ fromJson(needs.plan.outputs.wave-plans)['wave-2'] }} uses: genlayerlabs/genlayer-e2e/.github/workflows/e2e-run.yml@main @@ -434,7 +605,7 @@ jobs: # reliable trip signal — failure-label is only used for the # tile label content above, not the trip decision. component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} job-name: ${{ matrix.job-name || matrix.component }} stack-target: ${{ matrix.stack-target || 'dev-env' }} setup-task: ${{ matrix.setup-task }} @@ -442,6 +613,7 @@ jobs: tags: ${{ matrix.tags }} split: ${{ matrix.split }} features-source: ${{ matrix.features-source }} + e2e-dir: ${{ needs.plan.outputs.e2e-dir }} retry: ${{ matrix.retry }} max-shard-split: ${{ matrix.max-shard-split || 0 }} failure-tag: ${{ matrix.failure-tag || '' }} @@ -456,16 +628,20 @@ jobs: genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} + genlayer-project-boilerplate-ref: ${{ needs.plan.outputs.genlayer-project-boilerplate-ref }} + vscode-extension-ref: ${{ needs.plan.outputs.vscode-extension-ref }} harness-ref: ${{ needs.plan.outputs.harness-ref }} harness-sha: ${{ needs.plan.outputs.harness-sha }} cache-key: ${{ needs.plan.outputs.full-cache-key }} studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} + combo-cache-bust: ${{ needs.acknowledge.outputs.combo-cache-bust-token || needs.dispatch-context.outputs.combo-cache-bust || inputs.combo-cache-bust || '' }} github-retry-max: ${{ needs.plan.outputs.github-retry-max }} github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} @@ -478,7 +654,8 @@ jobs: # but didn't emit a failure-tag (e.g. internal job-level error # before conclusion ran). name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-2.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-2.outputs.failure-label) || (matrix.component != 'none' && (needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure')) && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1, wave-2] + needs: + [acknowledge, dispatch-context, plan, build, build-studio, wave-1, wave-2] # No cascade gate — wave-3 always runs. If build or any upstream # wave failed, with.component is overridden to 'none' below, # tripping e2e-run.yml's sentinel path (no GCE provisioned, @@ -497,7 +674,7 @@ jobs: # for reliability — failure-label is only consumed for the # tile label content above, not the trip decision. component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} job-name: ${{ matrix.job-name || matrix.component }} stack-target: ${{ matrix.stack-target || 'dev-env' }} setup-task: ${{ matrix.setup-task }} @@ -505,6 +682,7 @@ jobs: tags: ${{ matrix.tags }} split: ${{ matrix.split }} features-source: ${{ matrix.features-source }} + e2e-dir: ${{ needs.plan.outputs.e2e-dir }} retry: ${{ matrix.retry }} max-shard-split: ${{ matrix.max-shard-split || 0 }} failure-tag: ${{ matrix.failure-tag || '' }} @@ -519,16 +697,20 @@ jobs: genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} + genlayer-project-boilerplate-ref: ${{ needs.plan.outputs.genlayer-project-boilerplate-ref }} + vscode-extension-ref: ${{ needs.plan.outputs.vscode-extension-ref }} harness-ref: ${{ needs.plan.outputs.harness-ref }} harness-sha: ${{ needs.plan.outputs.harness-sha }} cache-key: ${{ needs.plan.outputs.full-cache-key }} studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} + combo-cache-bust: ${{ needs.acknowledge.outputs.combo-cache-bust-token || needs.dispatch-context.outputs.combo-cache-bust || inputs.combo-cache-bust || '' }} github-retry-max: ${{ needs.plan.outputs.github-retry-max }} github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} @@ -538,7 +720,17 @@ jobs: # (first match wins): build → wave-1 → wave-2 → wave-3 failure- # label, then generic-fallback via `.result == 'failure'`. name: ${{ (matrix.component != 'none' && ((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success'))) && format('{0} (skipped - build fails)', matrix.job-name) || (matrix.component != 'none' && needs.wave-1.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-1.outputs.failure-label) || (matrix.component != 'none' && needs.wave-2.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-2.outputs.failure-label) || (matrix.component != 'none' && needs.wave-3.outputs.failure-label != '') && format('{0} (skipped - {1} fails)', matrix.job-name, needs.wave-3.outputs.failure-label) || (matrix.component != 'none' && (needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure' || needs.wave-3.result == 'failure')) && format('{0} (skipped - fails)', matrix.job-name) || matrix.job-name }} - needs: [acknowledge, plan, build, build-studio, wave-1, wave-2, wave-3] + needs: + [ + acknowledge, + dispatch-context, + plan, + build, + build-studio, + wave-1, + wave-2, + wave-3, + ] # No cascade gate — wave-4 always runs. See wave-2 for the # cascade-as-sentinel rationale. if: | @@ -553,7 +745,7 @@ jobs: # Cascade override: force sentinel path when any upstream layer # (build / wave-1 / wave-2 / wave-3) failed. component: ${{ (((matrix.stack-target == 'studio' && needs.build-studio.outputs.build-status != 'success') || (matrix.stack-target != 'studio' && needs.build.outputs.build-status != 'success')) || needs.wave-1.result == 'failure' || needs.wave-2.result == 'failure' || needs.wave-3.result == 'failure') && 'none' || matrix.component }} - track: ${{ github.ref_name }} + track: ${{ needs.plan.outputs.track }} job-name: ${{ matrix.job-name || matrix.component }} stack-target: ${{ matrix.stack-target || 'dev-env' }} setup-task: ${{ matrix.setup-task }} @@ -561,6 +753,7 @@ jobs: tags: ${{ matrix.tags }} split: ${{ matrix.split }} features-source: ${{ matrix.features-source }} + e2e-dir: ${{ needs.plan.outputs.e2e-dir }} retry: ${{ matrix.retry }} max-shard-split: ${{ matrix.max-shard-split || 0 }} failure-tag: ${{ matrix.failure-tag || '' }} @@ -575,16 +768,20 @@ jobs: genlayer-testing-suite-ref: ${{ needs.plan.outputs.genlayer-testing-suite-ref }} genvm-linter-ref: ${{ needs.plan.outputs.genvm-linter-ref }} genlayer-wallet-ref: ${{ needs.plan.outputs.genlayer-wallet-ref }} + genlayer-project-boilerplate-ref: ${{ needs.plan.outputs.genlayer-project-boilerplate-ref }} + vscode-extension-ref: ${{ needs.plan.outputs.vscode-extension-ref }} harness-ref: ${{ needs.plan.outputs.harness-ref }} harness-sha: ${{ needs.plan.outputs.harness-sha }} cache-key: ${{ needs.plan.outputs.full-cache-key }} studio-cache-key: ${{ needs.plan.outputs.studio-cache-key }} profile: ${{ needs.plan.outputs.profile }} - pr-number: ${{ github.event.issue.number || github.run_id }} - comment-id: ${{ github.event.comment.id || '' }} - target-repo: ${{ github.repository }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id || '' }} - head-sha: ${{ needs.acknowledge.outputs.head-sha || github.sha }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id || '' }} + target-repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id || '' }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} + head-sha: ${{ needs.acknowledge.outputs.head-sha || needs.dispatch-context.outputs.head-sha || github.sha }} + combo-cache-bust: ${{ needs.acknowledge.outputs.combo-cache-bust-token || needs.dispatch-context.outputs.combo-cache-bust || inputs.combo-cache-bust || '' }} github-retry-max: ${{ needs.plan.outputs.github-retry-max }} github-retry-initial-delay: ${{ needs.plan.outputs.github-retry-initial-delay }} github-retry-max-delay: ${{ needs.plan.outputs.github-retry-max-delay }} @@ -604,6 +801,7 @@ jobs: result: needs: - acknowledge + - dispatch-context - plan - build - build-studio @@ -655,7 +853,7 @@ jobs: continue-on-error: true uses: actions/download-artifact@v8 with: - pattern: e2e-test-conclusion-*-pr${{ github.event.issue.number || github.run_id }} + pattern: e2e-test-conclusion-*-pr${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} path: /tmp/test-conclusions - name: Aggregate outcomes @@ -698,7 +896,7 @@ jobs: # the exact artifact path; on workflow_dispatch (no PR) the # run_id stands in, matching the wave jobs' pr-number input. CONCLUSIONS_DIR: /tmp/test-conclusions - PR_NUMBER: ${{ github.event.issue.number || github.run_id }} + PR_NUMBER: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number || github.run_id }} run: ./taskfiles/runner/scripts/aggregate-wave-outcomes.sh # Final notify-outcome — flips 👀 → 🚀 (overall pass) or 👎 @@ -717,17 +915,18 @@ jobs: # — no extra signal, and it buries the specific error message # under a generic-looking second comment. - name: Notify final outcome - if: always() && needs.acknowledge.outputs.check-run-id != '' && needs.plan.result != 'failure' + if: always() && (needs.acknowledge.outputs.check-run-id != '' || needs.dispatch-context.outputs.check-run-id != '') && needs.plan.result != 'failure' continue-on-error: true uses: genlayerlabs/genlayer-e2e/.github/actions/notify-outcome@main with: outcome: ${{ steps.aggregate.outcome == 'success' && 'success' || 'failure' }} check-run-title: 'E2E Tests' - repo: ${{ github.repository }} - comment-id: ${{ github.event.comment.id }} - check-run-id: ${{ needs.acknowledge.outputs.check-run-id }} - pr-number: ${{ github.event.issue.number }} + repo: ${{ needs.dispatch-context.outputs.target-repo || github.repository }} + comment-id: ${{ needs.dispatch-context.outputs.comment-id || github.event.comment.id }} + check-run-id: ${{ needs.acknowledge.outputs.check-run-id || needs.dispatch-context.outputs.check-run-id }} + pr-number: ${{ needs.dispatch-context.outputs.pr-number || github.event.issue.number }} github-token: ${{ steps.app-token.outputs.token }} + public-result-policy: ${{ needs.dispatch-context.outputs.public-result-policy || 'detailed' }} # Re-propagate the aggregate's exit status to the workflow # conclusion. Without this, the result job would always succeed