# GitHub Actions CI/CD (/docs/use-cases/ci-cd)

<!-- agent-signals: reading_time_min: 6 · est_tokens: 3680 · updated: 2026-07-30 -->
Related: [Coding Agents](/docs/use-cases/coding-agents.md), [Computer use](/docs/use-cases/computer-use.md), [Run Kubernetes with k3s](/docs/use-cases/k3s.md), [Cloud browser](/docs/use-cases/remote-browser.md)

CI/CD pipelines can use AI agents to review pull requests, generate tests, and validate code changes automatically. E2B sandboxes provide the secure, isolated execution environment where these agents can safely clone repositories, run untrusted code, and report results — all triggered by [GitHub Actions](https://docs.github.com/en/actions) on every pull request. Each run uses its own isolated sandbox, so malicious or buggy PR code never touches your CI runner.

## GitHub Actions workflow [#github-actions-workflow]

The workflow triggers on pull request events and runs a review script. `E2B_API_KEY` and the LLM API key are stored as [GitHub Actions secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions), while the built-in `GITHUB_TOKEN` is available automatically. The `permissions` block grants write access so the script can post PR comments.

<CodeGroup>
  <CodeBlockTabs defaultValue=".github/workflows/ai-review.yml (JavaScript)" groupId="-github-workflows-ai-review-yml-javascript-+-github-workflows-ai-review-yml-python-">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value=".github/workflows/ai-review.yml (JavaScript)">
        .github/workflows/ai-review.yml (JavaScript)
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value=".github/workflows/ai-review.yml (Python)">
        .github/workflows/ai-review.yml (Python)
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value=".github/workflows/ai-review.yml (JavaScript)">
      ```yaml  
      name: AI Code Review

      on:
        pull_request:
          types: [opened, synchronize]

      permissions:
        pull-requests: write

      jobs:
        ai-review:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4

            - name: Set up Node.js
              uses: actions/setup-node@v4
              with:
                node-version: "20"

            - name: Install dependencies
              run: npm install e2b openai

            - name: Run AI review
              env:
                E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
                OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
                PR_BRANCH: ${{ github.event.pull_request.head.ref }}
                PR_NUMBER: ${{ github.event.pull_request.number }}
                GITHUB_REPOSITORY: ${{ github.repository }}
              run: node review.mjs
      ```
    </CodeBlockTab>

    <CodeBlockTab value=".github/workflows/ai-review.yml (Python)">
      ```yaml  
      name: AI Code Review

      on:
        pull_request:
          types: [opened, synchronize]

      permissions:
        pull-requests: write

      jobs:
        ai-review:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4

            - name: Set up Python
              uses: actions/setup-python@v5
              with:
                python-version: "3.12"

            - name: Install dependencies
              run: pip install e2b openai

            - name: Run AI review
              env:
                E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
                OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
                GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
                PR_BRANCH: ${{ github.event.pull_request.head.ref }}
                PR_NUMBER: ${{ github.event.pull_request.number }}
                GITHUB_REPOSITORY: ${{ github.repository }}
              run: python review.py
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

## Review script [#review-script]

The workflow calls this script on every PR. It runs five steps inside an E2B sandbox, keeping all untrusted code isolated from the CI runner.

<CodeGroup>
  <CodeBlockTabs defaultValue="review.mjs" groupId="review-mjs+review-py">
    <CodeBlockTabsList>
      <CodeBlockTabsTrigger value="review.mjs">
        review.mjs
      </CodeBlockTabsTrigger>

      <CodeBlockTabsTrigger value="review.py">
        review.py
      </CodeBlockTabsTrigger>
    </CodeBlockTabsList>

    <CodeBlockTab value="review.mjs">
      ```typescript expandable  
      import { Sandbox, CommandExitError } from 'e2b'
      import OpenAI from 'openai'

      // --- 1. Create sandbox ---
      const sandbox = await Sandbox.create({ timeoutMs: 300_000 })
      console.log('Sandbox created:', sandbox.sandboxId)

      // --- 2. Clone the PR branch ---
      const repoUrl = `https://github.com/${process.env.PR_REPO}.git`

      await sandbox.git.clone(repoUrl, {
        path: '/home/user/repo',
        branch: process.env.PR_BRANCH,
        username: 'x-access-token',
        password: process.env.GITHUB_TOKEN,
        depth: 1,
      })
      console.log('Repository cloned')

      // --- 3. Get the diff and send it to an LLM for review ---
      const diffResult = await sandbox.commands.run(
        'cd /home/user/repo && git diff origin/main...HEAD'
      )

      const openai = new OpenAI()
      const response = await openai.chat.completions.create({
        model: 'gpt-5.2-mini',
        messages: [
          {
            role: 'system',
            content:
              'You are a senior code reviewer. Analyze the following git diff and provide a concise review with actionable feedback. Focus on bugs, security issues, and code quality.',
          },
          {
            role: 'user',
            content: `Review this diff:\n\n${diffResult.stdout}`,
          },
        ],
      })

      const review = response.choices[0].message.content
      console.log('AI Review:', review)

      // --- 4. Run the test suite inside the sandbox ---
      await sandbox.commands.run('cd /home/user/repo && npm install', {
        onStdout: (data) => console.log(data),
        onStderr: (data) => console.error(data),
      })

      try {
        await sandbox.commands.run('cd /home/user/repo && npm test', {
          onStdout: (data) => console.log(data),
          onStderr: (data) => console.error(data),
        })
        console.log('All tests passed')
      } catch (err) {
        if (err instanceof CommandExitError) {
          console.error('Tests failed with exit code:', err.exitCode)
          await sandbox.kill()
          process.exit(1)
        }
        throw err
      }

      // --- 5. Post results as a PR comment ---
      const prNumber = process.env.PR_NUMBER
      const repo = process.env.GITHUB_REPOSITORY

      await fetch(
        `https://api.github.com/repos/${repo}/issues/${prNumber}/comments`,
        {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            body: `## AI Code Review\n\n${review}`,
          }),
        }
      )

      await sandbox.kill()
      console.log('Done')
      ```
    </CodeBlockTab>

    <CodeBlockTab value="review.py">
      ```python expandable  
      import os
      import sys
      import requests
      from e2b import Sandbox, CommandExitException
      from openai import OpenAI

      # --- 1. Create sandbox ---
      sandbox = Sandbox.create(timeout=300)
      print(f"Sandbox created: {sandbox.sandbox_id}")

      # --- 2. Clone the PR branch ---
      repo_url = f"https://github.com/{os.environ['PR_REPO']}.git"

      sandbox.git.clone(
          repo_url,
          path="/home/user/repo",
          branch=os.environ["PR_BRANCH"],
          username="x-access-token",
          password=os.environ["GITHUB_TOKEN"],
          depth=1,
      )
      print("Repository cloned")

      # --- 3. Get the diff and send it to an LLM for review ---
      diff_result = sandbox.commands.run(
          "cd /home/user/repo && git diff origin/main...HEAD"
      )

      client = OpenAI()
      response = client.chat.completions.create(
          model="gpt-5.2-mini",
          messages=[
              {
                  "role": "system",
                  "content": "You are a senior code reviewer. Analyze the following git diff and provide a concise review with actionable feedback. Focus on bugs, security issues, and code quality.",
              },
              {
                  "role": "user",
                  "content": f"Review this diff:\n\n{diff_result.stdout}",
              },
          ],
      )

      review = response.choices[0].message.content
      print("AI Review:", review)

      # --- 4. Run the test suite inside the sandbox ---
      sandbox.commands.run(
          "cd /home/user/repo && npm install",
          on_stdout=lambda data: print(data),
          on_stderr=lambda data: print(data, file=sys.stderr),
      )

      try:
          sandbox.commands.run(
              "cd /home/user/repo && npm test",
              on_stdout=lambda data: print(data),
              on_stderr=lambda data: print(data, file=sys.stderr),
          )
          print("All tests passed")
      except CommandExitException as err:
          print(f"Tests failed with exit code: {err.exit_code}", file=sys.stderr)
          sandbox.kill()
          sys.exit(1)

      # --- 5. Post results as a PR comment ---
      pr_number = os.environ["PR_NUMBER"]
      repo = os.environ["GITHUB_REPOSITORY"]

      requests.post(
          f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
          headers={
              "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}",
              "Content-Type": "application/json",
          },
          json={
              "body": f"## AI Code Review\n\n{review}",
          },
      )

      sandbox.kill()
      print("Done")
      ```
    </CodeBlockTab>
  </CodeBlockTabs>
</CodeGroup>

1. **Create sandbox** — `Sandbox.create()` creates an isolated Linux environment for the review
2. **Clone the PR** — `sandbox.git.clone()` checks out the PR branch using `x-access-token` + `GITHUB_TOKEN` for [authentication](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation)
3. **AI review** — runs `git diff` inside the sandbox, sends the output to an LLM — swap the model for any provider via [Connect LLMs](/docs/quickstart/connect-llms)
4. **Run tests** — `commands.run()` streams output in real time and throws on failure (`CommandExitError` / `CommandExitException`)
5. **Post results** — comments the review on the PR via the GitHub REST API, then shuts down the sandbox

## Related guides [#related-guides]

<CardGroup cols="3">
  <Card title="Git integration" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M16 6.99998L19.0664 9.64296C20.3554 10.7541 21 11.3096 21 12C21 12.6903 20.3555 13.2459 19.0664 14.357L16 17&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M8 6.99998L4.93365 9.64296C3.64455 10.7541 3 11.3096 3 12C3 12.6903 3.64455 13.2459 4.93365 14.357L8 17&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="/docs/sandbox/git-integration">
    Clone repos, manage branches, and push changes from sandboxes
  </Card>

  <Card title="Connect LLMs" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M16.998 7.12652C17.3182 7.04393 17.654 7 18 7C20.2091 7 22 8.79086 22 11C22 13.2091 20.2091 15 18 15C17.6451 15 17.3009 14.9538 16.9733 14.867M16.998 7.12652C16.9993 7.08451 17 7.04233 17 7C17 4.79086 15.2091 3 13 3C11.0824 3 9.47994 4.34939 9.09041 6.15043M16.998 7.12652C16.9769 7.80763 16.7854 8.44584 16.4649 9M16.9733 14.867C16.9909 14.7472 17 14.6247 17 14.5C17 13.2905 16.1411 12.2816 15 12.05M16.9733 14.867C16.7957 16.0737 15.756 17 14.5 17H14C11.7909 17 10 18.7909 10 21M9.09041 6.15043C8.74377 6.05243 8.37801 6 8 6C5.79086 6 4 7.79086 4 10C4 10.3886 4.05542 10.7643 4.15878 11.1195M9.09041 6.15043C10.1015 6.43625 10.9498 7.10965 11.4649 8M4.15878 11.1195C2.9114 11.4832 2 12.6352 2 14C2 15.6569 3.34315 17 5 17C6.30622 17 7.41746 16.1652 7.82929 15M4.15878 11.1195C4.24921 11.4303 4.37632 11.7255 4.53513 12&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M11.8361 11.7435C11.3257 12.2353 10.453 12.3202 9.70713 11.9008C8.9612 11.4814 8.58031 10.6917 8.73535 10&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="/docs/quickstart/connect-llms">
    Integrate AI models with sandboxes using tool calling
  </Card>

  <Card title="Custom templates" icon="<svg xmlns=&#x22;http://www.w3.org/2000/svg&#x22; viewBox=&#x22;0 0 24 24&#x22; fill=&#x22;none&#x22;><path d=&#x22;M15 5H9C6.09433 5 4.64149 5 3.62036 5.73563C3.27976 5.981 2.981 6.27976 2.73563 6.62036C2 7.64149 2 9.09433 2 12C2 14.9057 2 16.3585 2.73563 17.3796C2.981 17.7202 3.27976 18.019 3.62036 18.2644C4.64149 19 6.09433 19 9 19H15C17.9057 19 19.3585 19 20.3796 18.2644C20.7202 18.019 21.019 17.7202 21.2644 17.3796C22 16.3585 22 14.9057 22 12C22 9.09433 22 7.64149 21.2644 6.62036C21.019 6.27976 20.7202 5.981 20.3796 5.73563C19.3585 5 17.9057 5 15 5Z&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M18 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M14 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M10 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/><path d=&#x22;M6 9V15&#x22; stroke=&#x22;currentColor&#x22; stroke-linecap=&#x22;round&#x22; stroke-linejoin=&#x22;round&#x22; stroke-width=&#x22;1.5&#x22;/></svg>" href="/docs/template/quickstart">
    Build reproducible sandbox environments for your pipelines
  </Card>
</CardGroup>
