From f7f8c864eebb359532c4896820e96dabedffbb53 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 08:38:04 +0000 Subject: [PATCH 1/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7ffb8e3f..cac7aeb1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -225,6 +225,12 @@ jobs: # 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-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 || '' }} # =========================================================================== # build — full stack up + pack to cache. Synthetic PR context on the From 52c5465fab5d968e3410f229c4de4947eea2d6a4 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:30:30 +0000 Subject: [PATCH 2/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cac7aeb1..fe5f4a08 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -185,6 +185,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: 'c930ed3f3db92eedf5b2bf944dbe882b5b8c962210e44ab33e8f397bb8bf64d1' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== From 0a2b48a12781c18dcdf9a06bb3d3b700e3814486 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:59:13 +0000 Subject: [PATCH 3/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fe5f4a08..3e9b55f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -192,7 +192,7 @@ jobs: # 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: 'c930ed3f3db92eedf5b2bf944dbe882b5b8c962210e44ab33e8f397bb8bf64d1' # SYNC_INJECT(template_hash) + template-hash: '3293b72724e1175541918c31e537e631a746138219ccff8f18fd4ac668238f24' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== @@ -283,7 +283,7 @@ jobs: # 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 }}"}} + {"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: ${{ github.event.issue.number || github.run_id }} comment-id: ${{ github.event.comment.id || '' }} target-repo: ${{ github.repository }} @@ -400,6 +400,8 @@ 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 }} @@ -470,6 +472,8 @@ 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 }} @@ -533,6 +537,8 @@ 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 }} @@ -589,6 +595,8 @@ 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 }} From 4fbdf9af3394bba858ce0c5203fa569bfad0deb8 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:09:09 +0000 Subject: [PATCH 4/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 238 +++++++++++++++++++++++++++++--------- 1 file changed, 183 insertions(+), 55 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3e9b55f9..0c3f06b0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -21,6 +21,10 @@ # on the consumer's repo. The acknowledge job's `if:` gates on # author-association so only members/owners/collaborators can fire # the pipeline. +# - `repository_dispatch` — 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` — manual / debug in this repo (UI dropdowns). # Acknowledge is skipped (no PR comment context); plan/build/waves # run with workflow_dispatch input values. @@ -70,6 +74,10 @@ 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' && format('E2E Test {0}/{1}/{2}/{3}', inputs.profile, inputs.track, inputs.scope, inputs.stack)) || format('noop {0}', github.run_id) }} @@ -77,6 +85,8 @@ run-name: >- on: issue_comment: types: [created] + repository_dispatch: + types: [e2e-dispatch] workflow_dispatch: inputs: profile: @@ -139,6 +149,10 @@ 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)) || format('e2e-noop-{0}', github.run_id) }} cancel-in-progress: true @@ -153,6 +167,112 @@ 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 emitting repository_dispatch. 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' + 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 }} + 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: ${{ toJson(github.event.client_payload.dispatch || github.event.client_payload) }} + run: | + set -euo pipefail + node services/e2e-dispatcher/scripts/validate-dispatch-payload.mjs "${PAYLOAD}" + echo "target_repo=$(jq -r '.target_repo' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "pr_number=$(jq -r '.pr_number' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "comment_id=$(jq -r '.comment_id // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "check_run_id=$(jq -r '.check_run_id' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "head_sha=$(jq -r '.head_sha' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + public_result_policy=$(jq -r '.public_result_policy // "detailed"' <<<"${PAYLOAD}") + echo "public_result_policy=${public_result_policy}" >> "$GITHUB_OUTPUT" + + - 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: ${{ github.event.client_payload.dispatch.comment_body || github.event.client_payload.comment_body }} + + - name: Validate Depends-On lines + id: validate-depends-on + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_BODY: ${{ github.event.client_payload.dispatch.pull_request_body || github.event.client_payload.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. # @@ -192,7 +312,7 @@ jobs: # 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: '3293b72724e1175541918c31e537e631a746138219ccff8f18fd4ac668238f24' # SYNC_INJECT(template_hash) + template-hash: '419a85c5c48a72efae81e7fdf1aeed963222a8bcf78f9ac82a51778e4c441c4a' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== @@ -205,40 +325,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 || '' }} + 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 @@ -247,7 +369,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 @@ -284,11 +406,11 @@ jobs: # 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 }}","genlayer-project-boilerplate":"${{ needs.plan.outputs.genlayer-project-boilerplate-ref }}","vscode-extension":"${{ needs.plan.outputs.vscode-extension-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 }} + 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 }} @@ -301,7 +423,7 @@ jobs: # =========================================================================== build-studio: name: build (studio) - needs: [acknowledge, plan] + needs: [acknowledge, dispatch-context, plan] if: | !cancelled() && needs.plan.result == 'success' && @@ -313,11 +435,11 @@ jobs: 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 @@ -341,7 +463,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 @@ -407,11 +529,12 @@ jobs: 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 }} 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 }} @@ -430,7 +553,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/ @@ -479,11 +602,12 @@ jobs: 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 }} 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 }} @@ -496,7 +620,7 @@ 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, @@ -544,11 +668,12 @@ jobs: 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 }} 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 }} @@ -558,7 +683,7 @@ 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: | @@ -602,11 +727,12 @@ jobs: 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 }} 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 }} @@ -626,6 +752,7 @@ jobs: result: needs: - acknowledge + - dispatch-context - plan - build - build-studio @@ -677,7 +804,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 @@ -720,7 +847,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 👎 @@ -739,17 +866,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 From f77ff7a1b9dc5f0883ed0b0a6468403138412b60 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:14:33 +0000 Subject: [PATCH 5/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 134 +++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 39 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0c3f06b0..ce95bf1f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,39 +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. -# - `repository_dispatch` — GitHub App dispatcher path. The webhook +# - `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` — 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` 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 # ------------------------------------------------ @@ -79,6 +76,11 @@ run-name: >- 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) }} @@ -98,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); @@ -131,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 @@ -153,6 +179,11 @@ concurrency: && 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 @@ -171,14 +202,14 @@ 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 emitting repository_dispatch. This job + # 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' + 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 }} @@ -191,6 +222,7 @@ jobs: 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: @@ -203,17 +235,26 @@ jobs: - name: Validate dispatcher payload id: payload env: - PAYLOAD: ${{ toJson(github.event.client_payload.dispatch || github.event.client_payload) }} + PAYLOAD: ${{ github.event_name == 'repository_dispatch' && toJson(github.event.client_payload.dispatch || github.event.client_payload) || inputs.dispatch_payload }} run: | set -euo pipefail node services/e2e-dispatcher/scripts/validate-dispatch-payload.mjs "${PAYLOAD}" echo "target_repo=$(jq -r '.target_repo' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" echo "pr_number=$(jq -r '.pr_number' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" echo "comment_id=$(jq -r '.comment_id // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "check_run_id=$(jq -r '.check_run_id' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "check_run_id=$(jq -r '.check_run_id // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" echo "head_sha=$(jq -r '.head_sha' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" + echo "combo_cache_bust=$(jq -r '.combo_cache_bust // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" public_result_policy=$(jq -r '.public_result_policy // "detailed"' <<<"${PAYLOAD}") echo "public_result_policy=${public_result_policy}" >> "$GITHUB_OUTPUT" + { + echo "comment_body<> "$GITHUB_OUTPUT" - name: Generate GitHub App token id: app-token @@ -232,13 +273,13 @@ jobs: id: parse uses: ./.github/actions/parse-comment with: - comment-body: ${{ github.event.client_payload.dispatch.comment_body || github.event.client_payload.comment_body }} + 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: ${{ github.event.client_payload.dispatch.pull_request_body || github.event.client_payload.pull_request_body || '' }} + PR_BODY: ${{ steps.payload.outputs.pull_request_body }} COMPONENTS_FILE: ${{ github.workspace }}/components.yaml run: | set -euo pipefail @@ -312,7 +353,7 @@ jobs: # 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: '419a85c5c48a72efae81e7fdf1aeed963222a8bcf78f9ac82a51778e4c441c4a' # SYNC_INJECT(template_hash) + template-hash: '515480228889995071837d4d5de2d703e0c2fa7a91370f34f17ad29c1a41bfd5' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== @@ -385,7 +426,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 }} @@ -399,7 +440,7 @@ 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 @@ -430,7 +471,7 @@ jobs: 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 }} @@ -500,7 +541,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 }} @@ -508,6 +549,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 || '' }} @@ -535,6 +577,7 @@ jobs: 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 }} @@ -563,7 +606,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 @@ -573,7 +623,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 }} @@ -581,6 +631,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 || '' }} @@ -608,6 +659,7 @@ jobs: 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 }} @@ -639,7 +691,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 }} @@ -647,6 +699,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 || '' }} @@ -674,6 +727,7 @@ jobs: 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 }} @@ -698,7 +752,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 }} @@ -706,6 +760,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 || '' }} @@ -733,6 +788,7 @@ jobs: 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 }} From ecf08f075124abb40b3d5abb11e0f279f0c082d1 Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:03:47 +0000 Subject: [PATCH 6/7] ci(workflows): sync e2e.yml from genlayer-e2e --- .github/workflows/e2e.yml | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index ce95bf1f..c83a2c36 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -236,25 +236,7 @@ jobs: id: payload env: PAYLOAD: ${{ github.event_name == 'repository_dispatch' && toJson(github.event.client_payload.dispatch || github.event.client_payload) || inputs.dispatch_payload }} - run: | - set -euo pipefail - node services/e2e-dispatcher/scripts/validate-dispatch-payload.mjs "${PAYLOAD}" - echo "target_repo=$(jq -r '.target_repo' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "pr_number=$(jq -r '.pr_number' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "comment_id=$(jq -r '.comment_id // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "check_run_id=$(jq -r '.check_run_id // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "head_sha=$(jq -r '.head_sha' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - echo "combo_cache_bust=$(jq -r '.combo_cache_bust // ""' <<<"${PAYLOAD}")" >> "$GITHUB_OUTPUT" - public_result_policy=$(jq -r '.public_result_policy // "detailed"' <<<"${PAYLOAD}") - echo "public_result_policy=${public_result_policy}" >> "$GITHUB_OUTPUT" - { - echo "comment_body<> "$GITHUB_OUTPUT" + run: bash services/e2e-dispatcher/scripts/emit-dispatch-payload-outputs.sh - name: Generate GitHub App token id: app-token @@ -353,7 +335,7 @@ jobs: # 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: '515480228889995071837d4d5de2d703e0c2fa7a91370f34f17ad29c1a41bfd5' # SYNC_INJECT(template_hash) + template-hash: '0bea0e74a739169d8dedf19fb3b4300aed42dadd43a806438a3fae0ebe8f8f87' # SYNC_INJECT(template_hash) secrets: inherit # =========================================================================== @@ -672,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, dispatch-context, 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, @@ -737,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, dispatch-context, 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: | From 09f2ca083b4bd7599c041db6c8c99ae861a5c70b Mon Sep 17 00:00:00 2001 From: "ci-core-e2e-runner[bot]" <263344042+ci-core-e2e-runner[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:06:25 +0000 Subject: [PATCH 7/7] ci(workflows): sync e2e.yml from genlayer-e2e