Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 20 additions & 100 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1186,8 +1186,10 @@
retention-days: 7

# - We skip optional tests on release branches
# - The tests run in optional-e2e.yml, so their failures do not mark the PR as failed. This job only waits for
# them and reports failures in a PR comment.
job_optional_e2e_tests:
name: E2E ${{ matrix.label || matrix.test-application }} Test (optional)
name: E2E Tests (optional)
# We only run E2E tests for non-fork PRs because the E2E tests require secrets to work and they can't be accessed from forks
# We need to add the `always()` check here because the previous step has this as well :(
# See: https://github.com/actions/runner/issues/2205
Expand All @@ -1196,35 +1198,14 @@
needs.job_build.outputs.e2e-matrix-optional != '{"include":[]}' && (github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
needs: [job_get_metadata, job_build, job_build_tarballs]
runs-on:
namespace-profile-sentry-javascript;overrides.cache-tag=sentry-javascript-e2e${{ github.event_name ==
'pull_request' && '-pr' || '' }}
timeout-minutes: 15
env:
E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }}
E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
# Needed because some apps expect a certain prefix
NEXT_PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
REACT_APP_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks'
E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests'
E2E_OPENROUTER_API_KEY: ${{ secrets.E2E_OPENROUTER_API_KEY }}
# Only this app talks to OpenAI directly (OpenRouter has no Decisions route), so only it gets the key.
E2E_OPENAI_DECISIONS_API_KEY:
${{ matrix.test-application == 'node-openai-decisions-send-to-sentry' && secrets.E2E_OPENAI_DECISIONS_API_KEY ||
'' }}
# Used by test apps that deploy a real Cloudflare Worker, e.g. cloudflare-workers-send-to-sentry
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md
RUNTIME: ${{ matrix.runtime }}
# Without the root node_modules, `pnpm run` would first install the whole workspace.
pnpm_config_verify_deps_before_run: 'false'
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.job_build.outputs.e2e-matrix-optional) }}

# This job only waits for optional-e2e.yml, which can take most of an hour. GitHub runners are free for public
# repos and Namespace runners are paid per minute, so the wait runs here and the tests run on Namespace.
runs-on: ubuntu-24.04
timeout-minutes: 60
permissions:
contents: read
actions: write
pull-requests: write
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v7
Expand All @@ -1233,80 +1214,19 @@
- name: Set up Node
uses: actions/setup-node@v7
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Bun
if:
matrix.test-application == 'node-exports-test-app' || contains(matrix.test-application, 'bun') ||
matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.14'
- name: Set up Deno
if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
# Keeps the pnpm store on the cache volume for the `pnpm install` of the test application
- name: Mount pnpm store
uses: ./.github/actions/mount-pnpm-store
node-version-file: 'package.json'
- name: Install pnpm
uses: ./.github/actions/install-pnpm

- name: Restore tarball artifacts
uses: actions/download-artifact@v8
# The Namespace cache volume is not available here, so only install what the action needs
- name: Install action dependencies
run: pnpm install --frozen-lockfile --filter @sentry-internal/optional-e2e-gh-action
- name: Run optional E2E tests
uses: ./dev-packages/optional-e2e-gh-action
with:
name: build-tarball-output
path: ${{ env.TARBALL_ARTIFACT_DOWNLOAD_PATH }}

- name: Restore test-utils tarball artifact
uses: actions/download-artifact@v8
with:
name: build-test-utils-tarball-output
path: dev-packages/test-utils

- name: Prepare E2E tests
run: pnpm run test:prepare
working-directory: dev-packages/e2e-tests

- name: Validate test setup
run: pnpm run test:validate
working-directory: dev-packages/e2e-tests

- name: Copy to temp
run:
pnpm run ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp
}}/test-application
working-directory: dev-packages/e2e-tests

- name: Add pnpm overrides
run:
pnpm run ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
}}/dev-packages/e2e-tests/packed
working-directory: dev-packages/e2e-tests

# Installs Playwright while the app builds. Keep it right before the build: the runner sets up a background
# composite action's steps from the job's shared `github` context, which fails with "Collection was modified" if
# a foreground step starts at that moment. Starting next to `download-artifact` also made it fail to read the event file.
- name: Install Playwright
id: install-playwright
background: true
uses: ./.github/actions/install-playwright
with:
browsers: chromium
cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}

- name: Build E2E app
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
run: ${{ matrix.build-command || 'pnpm test:build' }}

- wait: [install-playwright]

- name: Run E2E test
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 10
run: ${{ matrix.assert-command || 'pnpm test:assert' }}
github_token: ${{ secrets.GITHUB_TOKEN }}
commit: ${{ env.HEAD_COMMIT }}
matrix: ${{ needs.job_build.outputs.e2e-matrix-optional }}

Check failure on line 1229 in .github/workflows/build.yml

View check run for this annotation

@sentry/warden / warden: security-review

Unvalidated workflow_dispatch matrix enables shell execution with E2E secrets

A user who can manually dispatch this workflow can set `matrix.build-command` or `assert-command` to arbitrary shell commands. Those commands run in a job whose environment contains Cloudflare, Sentry, and API credentials, allowing the user to exfiltrate or misuse them.
job_required_jobs_passed:
name: All required jobs passed or were skipped
needs:
Expand Down
167 changes: 167 additions & 0 deletions .github/workflows/optional-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
name: 'CI: Optional E2E Tests'
run-name: Optional E2E tests for ${{ inputs.commit }} (${{ inputs.correlation_id }})

# Dispatched by `job_optional_e2e_tests` in build.yml. This is a separate workflow because GitHub
# does not count failed checks of a dispatched run in the PR status, so these failures do not mark
# the PR as failed.
on:
# Only here so that GitHub registers this workflow before it is on the default branch, which a
# dispatch needs. The job is skipped for this event. Remove this trigger after the merge.
pull_request:
workflow_dispatch:
inputs:
commit:
description: The commit that the tarballs were built from
required: true
build_run_id:
description: The build.yml run that uploaded the tarball artifacts
required: true
matrix:
description: The optional E2E test matrix as JSON
required: true
pr_number:
description: The PR that the run is for, if there is one
required: false
correlation_id:
description: Lets the dispatching job find this run
required: true

# Like build.yml: a new push to a PR cancels the old run, other runs are never cancelled.
concurrency:
group: ${{ github.workflow }}-${{ inputs.pr_number || github.run_id }}
cancel-in-progress: true

env:
HEAD_COMMIT: ${{ inputs.commit }}

# See build.yml
TARBALL_ARTIFACT_DOWNLOAD_PATH: ${{ github.workspace }}/packages
DISABLE_V8_COMPILE_CACHE: '1'

jobs:
job_optional_e2e_tests:
name: E2E ${{ matrix.label || matrix.test-application }} Test (optional)
if: github.event_name == 'workflow_dispatch'
# Same cache volume as the E2E jobs in build.yml. The event is always `workflow_dispatch`, so the PR number
# decides if the run gets the PR volume.
runs-on:
namespace-profile-sentry-javascript;overrides.cache-tag=sentry-javascript-e2e${{ inputs.pr_number != '' && '-pr'
|| '' }}
timeout-minutes: 15
permissions:
contents: read
# To download the artifacts of the build.yml run
actions: read
env:
E2E_TEST_AUTH_TOKEN: ${{ secrets.E2E_TEST_AUTH_TOKEN }}
E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
# Needed because some apps expect a certain prefix
NEXT_PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
PUBLIC_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
REACT_APP_E2E_TEST_DSN: ${{ secrets.E2E_TEST_DSN }}
E2E_TEST_SENTRY_ORG_SLUG: 'sentry-javascript-sdks'
E2E_TEST_SENTRY_PROJECT: 'sentry-javascript-e2e-tests'
E2E_OPENROUTER_API_KEY: ${{ secrets.E2E_OPENROUTER_API_KEY }}
# Only this app talks to OpenAI directly (OpenRouter has no Decisions route), so only it gets the key.
E2E_OPENAI_DECISIONS_API_KEY:
${{ matrix.test-application == 'node-openai-decisions-send-to-sentry' && secrets.E2E_OPENAI_DECISIONS_API_KEY ||
'' }}
# Used by test apps that deploy a real Cloudflare Worker, e.g. cloudflare-workers-send-to-sentry
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
# Keeps the Worker name at `<prefix>-pr-<number>`, which the cleanup-e2e-workers workflow deletes
E2E_PR_NUMBER: ${{ inputs.pr_number }}
# The `runtime` of a runtime variant, see dev-packages/e2e-tests/README.md
RUNTIME: ${{ matrix.runtime }}
# Without the root node_modules, `pnpm run` would first install the whole workspace.
pnpm_config_verify_deps_before_run: 'false'
strategy:
fail-fast: false
# The fallback only keeps the expression valid for the skipped `pull_request` run.
matrix: ${{ fromJson(inputs.matrix || '{"include":[{}]}') }}

steps:
- name: Check out commit ${{ env.HEAD_COMMIT }}
uses: actions/checkout@v7
with:
ref: ${{ env.HEAD_COMMIT }}
- name: Set up Node
uses: actions/setup-node@v7
with:
node-version-file: 'dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}/package.json'
- name: Set up Bun
if:
matrix.test-application == 'node-exports-test-app' || contains(matrix.test-application, 'bun') ||
matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.14'
- name: Set up Deno
if: contains(matrix.test-application, 'deno') || matrix.runtime == 'deno'
uses: denoland/setup-deno@v2.0.5
with:
deno-version: ${{ matrix.deno-version || 'v2.8.3' }}
# Keeps the pnpm store on the cache volume for the `pnpm install` of the test application
- name: Mount pnpm store
uses: ./.github/actions/mount-pnpm-store
- name: Install pnpm
uses: ./.github/actions/install-pnpm

- name: Restore tarball artifacts
uses: actions/download-artifact@v8
with:
name: build-tarball-output
path: ${{ env.TARBALL_ARTIFACT_DOWNLOAD_PATH }}
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}

- name: Restore test-utils tarball artifact
uses: actions/download-artifact@v8
with:
name: build-test-utils-tarball-output
path: dev-packages/test-utils
run-id: ${{ inputs.build_run_id }}
github-token: ${{ github.token }}

- name: Prepare E2E tests
run: pnpm run test:prepare
working-directory: dev-packages/e2e-tests

- name: Validate test setup
run: pnpm run test:validate
working-directory: dev-packages/e2e-tests

- name: Copy to temp
run:
pnpm run ci:copy-to-temp ./test-applications/${{ matrix.test-application }} ${{ runner.temp
}}/test-application
working-directory: dev-packages/e2e-tests

- name: Add pnpm overrides
run:
pnpm run ci:pnpm-overrides ${{ runner.temp }}/test-application ${{ github.workspace
}}/dev-packages/e2e-tests/packed
working-directory: dev-packages/e2e-tests

# Installs Playwright while the app builds. Keep it right before the build: the runner sets up a background
# composite action's steps from the job's shared `github` context, which fails with "Collection was modified" if
# a foreground step starts at that moment. Starting next to `download-artifact` also made it fail to read the event file.
- name: Install Playwright
id: install-playwright
background: true
uses: ./.github/actions/install-playwright
with:
browsers: chromium
cwd: dev-packages/e2e-tests/test-applications/${{ matrix.test-application }}

- name: Build E2E app
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 7
run: ${{ matrix.build-command || 'pnpm test:build' }}

- wait: [install-playwright]

- name: Run E2E test
working-directory: ${{ runner.temp }}/test-application
timeout-minutes: 10
run: ${{ matrix.assert-command || 'pnpm test:assert' }}
Comment thread
andreiborza marked this conversation as resolved.
4 changes: 4 additions & 0 deletions dev-packages/optional-e2e-gh-action/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "../../node_modules/oxlint/configuration_schema.json",
"extends": ["../.oxlintrc.json"]
}
15 changes: 15 additions & 0 deletions dev-packages/optional-e2e-gh-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: 'optional-e2e-gh-action'
description: 'Run the optional E2E tests in their own workflow, wait for them and report failures in a PR comment'
inputs:
github_token:
required: true
description: 'a github access token'
commit:
required: true
description: 'The commit that the tarballs were built from'
matrix:
required: true
description: 'The optional E2E test matrix as JSON'

Check failure on line 12 in dev-packages/optional-e2e-gh-action/action.yml

View check run for this annotation

@sentry/warden / warden: security-review

[MN9-6U9] Unvalidated workflow_dispatch matrix enables shell execution with E2E secrets (additional location)

A user who can manually dispatch this workflow can set `matrix.build-command` or `assert-command` to arbitrary shell commands. Those commands run in a job whose environment contains Cloudflare, Sentry, and API credentials, allowing the user to exfiltrate or misuse them.
runs:
using: 'node24'
main: 'index.mjs'

Check failure on line 15 in dev-packages/optional-e2e-gh-action/action.yml

View check run for this annotation

@sentry/warden / warden: security-review

Dispatch runs optional-e2e workflow YAML from the PR branch with secrets

The action dispatches optional-e2e.yml with `ref: GITHUB_HEAD_REF`, so an unmerged same-repo PR can replace that workflow and still receive repository secrets. Dispatch from the default branch and only use the PR commit for checking out test code.
Comment on lines +14 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatch runs optional-e2e workflow YAML from the PR branch with secrets

The action dispatches optional-e2e.yml with ref: GITHUB_HEAD_REF, so an unmerged same-repo PR can replace that workflow and still receive repository secrets. Dispatch from the default branch and only use the PR commit for checking out test code.

Evidence
  • build.yml checks out the PR commit and runs ./dev-packages/optional-e2e-gh-action (this action’s index.mjs).
  • createWorkflowDispatch sets ref: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME, so GitHub loads the workflow file from the PR branch tip.
  • optional-e2e.yml is workflow_dispatch-only for the job and injects production E2E secrets (CLOUDFLARE_API_TOKEN, API keys, auth token).
  • A same-repo PR can change .github/workflows/optional-e2e.yml on the branch and have that version execute with those secrets before merge.

Identified by Warden · security-review · QFB-HFN

Loading
Loading