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
99 changes: 11 additions & 88 deletions .github/workflows/build_prod_template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,107 +4,30 @@ on:
workflow_dispatch:
inputs:
target_environment:
description: Target environment
description: Target environment (`all` = every production cluster)
required: true
type: choice
default: foxtrot
default: all
options:
- all
- foxtrot
- staging
- juliett
- tango
- staging
skip_cache:
description: Skip build cache
required: false
type: boolean
default: false

concurrency:
group: Release-${{ github.ref }}-${{ inputs.target_environment }}
cancel-in-progress: false

permissions:
contents: read

jobs:
build-template:
name: Build E2B template
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '${{ env.TOOL_VERSION_PYTHON }}'

- name: Install development dependencies
working-directory: ./template
run: pip install -r requirements-dev.txt

- name: Resolve target environment
env:
TARGET_ENVIRONMENT: ${{ inputs.target_environment }}
FOXTROT_DOMAIN: ${{ vars.E2B_DOMAIN }}
FOXTROT_API_KEY: ${{ secrets.E2B_PROD_API_KEY }}
STAGING_API_KEY: ${{ secrets.E2B_STAGING_API_KEY }}
JULIETT_API_KEY: ${{ secrets.E2B_JULIETT_API_KEY }}
run: |
set -eu

case "$TARGET_ENVIRONMENT" in
foxtrot)
E2B_DOMAIN="$FOXTROT_DOMAIN"
E2B_API_KEY="$FOXTROT_API_KEY"
;;
staging)
E2B_DOMAIN="e2b-staging.dev"
E2B_API_KEY="$STAGING_API_KEY"
;;
juliett)
E2B_DOMAIN="e2b-juliett.dev"
E2B_API_KEY="$JULIETT_API_KEY"
;;
*)
echo "Unknown target environment: $TARGET_ENVIRONMENT" >&2
exit 1
;;
esac

if [ -z "$E2B_DOMAIN" ]; then
echo "Missing E2B domain for target environment: $TARGET_ENVIRONMENT" >&2
exit 1
fi

if [ -z "$E2B_API_KEY" ]; then
echo "Missing API key secret for target environment: $TARGET_ENVIRONMENT" >&2
exit 1
fi

echo "::add-mask::$E2B_API_KEY"

{
echo "E2B_DOMAIN=$E2B_DOMAIN"
echo "E2B_API_KEY=$E2B_API_KEY"
} >> "$GITHUB_ENV"

{
echo "### Build target"
echo
echo "Target: $TARGET_ENVIRONMENT"
echo "Domain: $E2B_DOMAIN"
} >> "$GITHUB_STEP_SUMMARY"

- name: Build E2B template
id: build-template
working-directory: ./template
run: |
python build_prod.py
env:
SKIP_CACHE: ${{ inputs.skip_cache }}
name: Build
uses: ./.github/workflows/build_prod_template_clusters.yml
with:
target: ${{ inputs.target_environment }}
skip_cache: ${{ inputs.skip_cache }}
secrets: inherit
112 changes: 112 additions & 0 deletions .github/workflows/build_prod_template_clusters.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Build Prod Template (clusters)

# Builds the production `code-interpreter-v1` template on E2B clusters. Every
# cluster is a separate tenancy with its own team, API key and template
# registry, so a template built on one cluster does not exist on the others.
# The cluster map (name, domain, API key secret) lives in the `plan` job.

on:
workflow_call:
inputs:
target:
description: Cluster to build on, or `all` for every production cluster.
required: false
type: string
default: all
skip_cache:
description: Skip build cache
required: false
type: boolean
default: false

permissions:
contents: read

jobs:
plan:
name: Select clusters
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- id: plan
env:
TARGET: ${{ inputs.target }}
run: |
MATRIX=$(jq -nc --arg target "$TARGET" '
[
{ cluster: "foxtrot", domain: "e2b.dev", api_key_secret: "E2B_PROD_API_KEY", production: true },
{ cluster: "juliett", domain: "e2b-juliett.dev", api_key_secret: "E2B_JULIETT_API_KEY", production: true },
{ cluster: "tango", domain: "e2b-tango.dev", api_key_secret: "E2B_TANGO_API_KEY", production: true },
{ cluster: "staging", domain: "e2b-staging.dev", api_key_secret: "E2B_STAGING_API_KEY", production: false }
]
| map(select(if $target == "all" then .production else .cluster == $target end))
| map(del(.production))
')
if [ "$MATRIX" = "[]" ]; then
echo "::error::Unknown cluster: $TARGET" >&2
exit 1
fi
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"

build-template:
name: Build E2B template (${{ matrix.cluster }})
needs: plan
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
# Builds on the same cluster serialize, across the release workflow and
# manual dispatches alike; different clusters run concurrently.
concurrency:
group: Release-${{ github.ref }}-${{ matrix.cluster }}
cancel-in-progress: false
env:
E2B_API_KEY: ${{ secrets[matrix.api_key_secret] }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: MEDIUM

The reusable cluster-build job selects the API key with a dynamic secrets lookup (secrets[matrix.api_key_secret]). GitHub cannot statically determine which secrets that expression needs, so the runner receives every secret inherited by the called workflow. Both callers pass secrets: inherit, so the release path now exposes unused high-value credentials (PyPI, Docker Hub, version-bumper private key, Slack webhooks, and other clusters’ API keys) to this job. E2B_API_KEY is also set at job scope, so every step—including third-party actions and pip install—sees it.

Impact: Compromise of any step on a template-build runner (hijacked Action, poisoned pip dependency, or later logging of context) can read the full inherited secret set instead of a single cluster API key.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit f8cb5ab. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: MEDIUM

The reusable cluster-build job selects the API key with a dynamic secrets lookup (secrets[matrix.api_key_secret]). GitHub cannot statically determine which secrets that expression needs, so the runner receives every secret inherited by the called workflow. Both callers pass secrets: inherit, so the release path now delivers unused high-value credentials (PyPI, Docker Hub, version-bumper private key, Slack webhooks, and other clusters’ API keys) to this job. E2B_API_KEY is also set at job scope, so every step—including third-party actions and pip install—sees the selected cluster key.

Impact: Compromise of any step on a template-build runner (hijacked Action, poisoned pip dependency, or later logging of context) can read the full inherited secret set instead of a single cluster API key.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 6bccfc0. Configure here.

E2B_DOMAIN: ${{ matrix.domain }}
steps:
- name: Check the cluster API key
env:
CLUSTER: ${{ matrix.cluster }}
API_KEY_SECRET: ${{ matrix.api_key_secret }}
run: |
if [ -z "$E2B_API_KEY" ]; then
echo "::error::Missing secret $API_KEY_SECRET for cluster: $CLUSTER" >&2
exit 1
fi

- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '${{ env.TOOL_VERSION_PYTHON }}'

- name: Install development dependencies
working-directory: ./template
run: pip install -r requirements-dev.txt

- name: Build E2B template
working-directory: ./template
run: python build_prod.py
env:
SKIP_CACHE: ${{ inputs.skip_cache }}

- name: Summarize
env:
CLUSTER: ${{ matrix.cluster }}
run: |
{
echo "### Build target"
echo
echo "Cluster: $CLUSTER"
echo "Domain: ${E2B_DOMAIN:-(default)}"
} >> "$GITHUB_STEP_SUMMARY"
52 changes: 21 additions & 31 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ on:
# makes a re-dispatch the recovery path too.
workflow_dispatch: {}

concurrency: Release-${{ github.ref }}-foxtrot
# One release at a time. The per-cluster template builds serialize against
# manual `Build Prod Template` runs through their own groups, inside the
# called workflow.
concurrency: Release-${{ github.ref }}

permissions:
id-token: write
Expand Down Expand Up @@ -223,46 +226,28 @@ jobs:
--push \
--tag ${{ secrets.DOCKERHUB_USERNAME }}/code-interpreter:latest -f - .

# Builds the template on every production cluster; a failure on any of them
# blocks the release.
build-template:
name: Build E2B template
runs-on: ubuntu-latest
needs: [preflight, build-docker-image]
if: (!cancelled()) &&
!contains(needs.*.result, 'failure') &&
(needs.preflight.outputs.template == 'true' || needs.preflight.outputs.charts == 'true')
steps:
- name: Checkout repository
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0

- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'

- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: '${{ env.TOOL_VERSION_PYTHON }}'

- name: Install development dependencies
working-directory: ./template
run: pip install -r requirements-dev.txt

- name: Build E2B template
id: build-template
working-directory: ./template
run: |
python build_prod.py
env:
E2B_API_KEY: ${{ secrets.E2B_PROD_API_KEY }}
E2B_DOMAIN: ${{ vars.E2B_DOMAIN }}
uses: ./.github/workflows/build_prod_template_clusters.yml
with:
target: all
secrets: inherit

release:
# Every upstream job is listed, not just the last one: a job that fails makes
# its dependents *skip*, and a skipped job is not a failure — so gating on
# `needs.*.result` only works for the jobs this one depends on directly.
needs: [preflight, charts-release, build-docker-image, build-template]
needs:
- preflight
- charts-release
- build-docker-image
- build-template
if: (!cancelled()) &&
!contains(needs.*.result, 'failure') &&
needs.preflight.outputs.release == 'true'
Expand Down Expand Up @@ -404,7 +389,12 @@ jobs:
report-failure:
# `preflight` included so a failure there is reported too, whether or not
# `failure()` looks past this job's direct dependencies.
needs: [preflight, charts-release, build-docker-image, build-template, release]
needs:
- preflight
- charts-release
- build-docker-image
- build-template
- release
if: failure()
name: Code Interpreter Release Failed - Slack Notification
runs-on: ubuntu-latest
Expand Down
Loading