diff --git a/.github/workflows/e2b-e2e.yml b/.github/workflows/e2b-e2e.yml new file mode 100644 index 0000000000..abbd0482ed --- /dev/null +++ b/.github/workflows/e2b-e2e.yml @@ -0,0 +1,58 @@ +name: E2E (E2B sandbox) + +# This suite provisions external E2B sandboxes and is intentionally opt-in. +# It has no push, pull_request, schedule, or workflow_call trigger. +on: + workflow_dispatch: + +permissions: + contents: read + +env: + # CI runs must never report to the production telemetry endpoint baked + # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). + DSH_TELEMETRY_DISABLED: '1' + +jobs: + e2b: + runs-on: ubuntu-latest + name: E2B live Loader composition + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install (immutable) + run: pnpm install --frozen-lockfile + + # The tests self-skip locally when the credential is absent. A manually + # dispatched run must fail instead of reporting an all-skipped green. + - name: Preflight (require E2B API key) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + run: | + set -euo pipefail + if [ -z "${E2B_API_KEY:-}" ]; then + echo "::error::E2B_API_KEY is empty. Configure the E2B_API_KEY_EXTERNAL repository secret." + exit 1 + fi + echo "E2B_API_KEY present." + + # The Loader smoke runs package exports under plain Node in lib mode. + - name: Build (lib for the E2B Loader smoke) + run: pnpm run build + + - name: E2B tests (live sandbox) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + DSH_E2E_MAX_WORKERS: '1' + DSH_EXAMPLE_MODE: lib + run: >- + pnpm exec vitest run --config vitest.e2e.config.ts + packages/e2b/e2b/tests/composition.e2e.ts diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index baeac7a8c0..0395fe8c36 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -28,6 +28,33 @@ describe('CI workflow', () => { }) }) +describe('E2B e2e workflow', () => { + it('is manual-only and fails loud before running the focused live suite', () => { + const workflow = loadWorkflow('.github/workflows/e2b-e2e.yml') + expect(workflow.on).toEqual({ workflow_dispatch: null }) + if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.e2b) || !Array.isArray(workflow.jobs.e2b.steps)) { + throw new TypeError('E2B e2e workflow must define the e2b job steps') + } + + const steps = workflow.jobs.e2b.steps.filter(isRecord) + const preflight = steps.find(step => step.name === 'Preflight (require E2B API key)') + const e2b = steps.find(step => step.name === 'E2B tests (live sandbox)') + + expect(preflight).toMatchObject({ + env: { E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}' }, + }) + expect(preflight?.run).toContain('E2B_API_KEY_EXTERNAL repository secret') + expect(e2b).toMatchObject({ + env: { + E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}', + DSH_E2E_MAX_WORKERS: '1', + DSH_EXAMPLE_MODE: 'lib', + }, + }) + expect(e2b?.run).toContain('packages/e2b/e2b/tests/composition.e2e.ts') + }) +}) + describe('Issue lifecycle workflow', () => { it('uses review signals instead of rerunning when a draft becomes ready', () => { const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')