> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ns.rocks/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub Actions CI/CD

> Run AI-powered code review, testing, and validation in secure Nullspace machines from GitHub Actions workflows.

CI/CD pipelines can use AI agents to review pull requests, generate tests, and
validate code changes automatically. Nullspace machines provide the isolated
Linux environment where automation can clone pull request code, install
dependencies, run untrusted tests, and report results without executing the PR
branch on the GitHub Actions runner.

This pattern keeps the runner responsible for trusted orchestration and PR
comments. The PR branch is fetched and tested only inside a disposable
Nullspace machine.

## GitHub Actions workflow

The workflow runs from the trusted base branch with `pull_request_target`, then
checks out the base commit so the review script itself cannot be replaced by
the pull request. Store `NULLSPACE_API_KEY`, `NULLSPACE_API_URL`, and
`OPENAI_API_KEY` as GitHub Actions secrets. For private repositories, add a
separate read-only `GIT_READ_TOKEN` secret for machine-side Git clone and fetch
operations.

```yaml theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
name: Nullspace AI Review

on:
  pull_request_target:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: read
  issues: write

jobs:
  ai-review:
    runs-on: ubuntu-latest
    steps:
      - name: Check out trusted workflow code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: python -m pip install "nullspace-sdk==1.0.0" openai requests

      - name: Run AI review in Nullspace
        env:
          NULLSPACE_API_KEY: ${{ secrets.NULLSPACE_API_KEY }}
          NULLSPACE_API_URL: ${{ secrets.NULLSPACE_API_URL }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          OPENAI_MODEL: gpt-5.2-mini
          GITHUB_TOKEN: ${{ github.token }}
          GIT_READ_TOKEN: ${{ secrets.GIT_READ_TOKEN }}
          GITHUB_REPOSITORY: ${{ github.repository }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
          NULLSPACE_TEMPLATE: base
          INSTALL_COMMAND: python3 -m pip install -e .
          TEST_COMMAND: python3 -m pytest
        run: python .github/scripts/nullspace_ai_review.py
```

## Review script

The workflow calls this script on every pull request. It creates a Nullspace
machine, fetches the PR branch inside the machine, sends the diff to an LLM,
runs your test command, and posts one PR comment with the review and validation
result.

```python theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
import os
import shlex
import sys

import requests
from nullspace import GitHttpsAuth, Machine
from openai import OpenAI

WORKDIR = "/workspace/repo"


def required(name: str) -> str:
    value = os.environ.get(name, "").strip()
    if not value:
        raise RuntimeError(f"{name} is required")
    return value


def run_checked(
    machine: Machine,
    command: str,
    *,
    cwd: str = WORKDIR,
    timeout: int = 300,
    stream: bool = True,
):
    result = machine.commands.run(
        command,
        shell=True,
        cwd=cwd,
        timeout=timeout,
        envs={"PYTHONUNBUFFERED": "1"},
        on_stdout=(lambda data: print(data, end="")) if stream else None,
        on_stderr=(lambda data: print(data, end="", file=sys.stderr)) if stream else None,
    )
    if result.exit_code != 0:
        details = result.stderr.strip() or result.stdout.strip()
        raise RuntimeError(f"command failed ({result.exit_code}): {command}\n{details}")
    return result


def review_diff(diff: str) -> str:
    if not diff.strip():
        return "No diff was detected for this pull request."

    max_chars = int(os.environ.get("MAX_DIFF_CHARS", "120000"))
    if len(diff) > max_chars:
        diff = diff[:max_chars] + "\n\n[Diff truncated before review.]"

    client = OpenAI()
    response = client.chat.completions.create(
        model=os.environ.get("OPENAI_MODEL", "gpt-5.2-mini"),
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a senior code reviewer. Provide concise, actionable "
                    "feedback. Focus on bugs, security issues, regressions, and "
                    "missing tests."
                ),
            },
            {"role": "user", "content": f"Review this diff:\n\n{diff}"},
        ],
    )
    return response.choices[0].message.content or "No review content returned."


def post_comment(body: str) -> None:
    repo = required("GITHUB_REPOSITORY")
    pr_number = required("PR_NUMBER")
    response = requests.post(
        f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
        headers={
            "Authorization": f"Bearer {required('GITHUB_TOKEN')}",
            "Accept": "application/vnd.github+json",
        },
        json={"body": body},
        timeout=30,
    )
    response.raise_for_status()


def main() -> int:
    repo = required("GITHUB_REPOSITORY")
    pr_number = required("PR_NUMBER")
    base_ref = required("PR_BASE_REF")
    template = os.environ.get("NULLSPACE_TEMPLATE", "base")
    machine_timeout = int(os.environ.get("NULLSPACE_MACHINE_TIMEOUT", "900"))

    repo_url = f"https://github.com/{repo}.git"
    pr_branch = f"pr-{pr_number}"
    git_read_token = os.environ.get("GIT_READ_TOKEN", "").strip()
    auth = (
        GitHttpsAuth(username="x-access-token", password=git_read_token)
        if git_read_token
        else None
    )

    test_summary = "Validation did not run."
    exit_status = 0

    with Machine.create(template=template, timeout=machine_timeout) as machine:
        print(f"Machine created: {machine.id}")

        if auth is not None:
            machine.git.dangerously_authenticate("x-access-token", git_read_token)

        machine.git.clone(repo_url, path=WORKDIR, branch=base_ref, depth=1, auth=auth)
        run_checked(
            machine,
            f"git fetch origin {shlex.quote(f'pull/{pr_number}/head:{pr_branch}')} --depth 100",
            timeout=120,
        )
        run_checked(machine, f"git checkout {shlex.quote(pr_branch)}", timeout=60)

        diff_result = run_checked(
            machine,
            f"git diff --no-ext-diff --unified=80 {shlex.quote(f'origin/{base_ref}')}..HEAD",
            timeout=120,
            stream=False,
        )
        review = review_diff(diff_result.stdout)

        try:
            install_command = os.environ.get("INSTALL_COMMAND", "").strip()
            if install_command:
                run_checked(machine, install_command, timeout=600)

            test_command = required("TEST_COMMAND")
            test_result = run_checked(machine, test_command, timeout=600)
            test_summary = (
                f"Passed `{test_command}` with exit code {test_result.exit_code}."
            )
        except RuntimeError as err:
            test_summary = f"Failed validation:\n\n{str(err)[-3500:]}"
            exit_status = 1

    post_comment(
        f"## Nullspace AI Code Review\n\n{review}\n\n"
        f"## Machine Validation\n\n{test_summary}"
    )
    return exit_status


if __name__ == "__main__":
    sys.exit(main())
```

## How it works

1. **Run trusted orchestration** — the GitHub runner checks out the base commit,
   installs the Nullspace SDK, and runs the review script from trusted code.
2. **Create machine** — `Machine.create()` starts a disposable Nullspace VM for
   the pull request.
3. **Fetch the PR** — `machine.git.clone()` checks out the base branch, then a
   machine-local `git fetch` loads `refs/pull/<number>/head`.
4. **AI review** — `commands.run()` generates a diff inside the machine and the
   runner sends it to an LLM.
5. **Run validation** — the script runs `INSTALL_COMMAND` and `TEST_COMMAND`
   inside the machine. Nullspace command results report `exit_code`, `stdout`,
   and `stderr`; non-zero exits are handled explicitly.
6. **Post results** — the runner posts one PR comment through the GitHub REST
   API after the machine closes.

## Security notes

* Do not check out the PR branch on the GitHub runner when using
  `pull_request_target`.
* Do not pass `GITHUB_TOKEN`, `OPENAI_API_KEY`, or `NULLSPACE_API_KEY` into the
  machine environment. The example keeps those values runner-side.
* Leave `GIT_READ_TOKEN` unset for public repositories. For private
  repositories, use a token scoped only for read access because PR code can read
  any credentials made available inside the machine.
* Build a custom Nullspace template for heavyweight CI dependencies such as
  browsers, language toolchains, or system packages.

## Related guides

<CardGroup cols={2}>
  <Card title="Git helpers" href="../reference/git">
    Clone repositories, manage branches, inspect diffs, and push changes from machines.
  </Card>

  <Card title="Commands" href="../commands/overview">
    Run foreground, streaming, and background commands inside a machine.
  </Card>

  <Card title="Custom templates" href="../templates/overview">
    Preinstall toolchains and CI dependencies for repeatable machine runs.
  </Card>

  <Card title="Agents" href="../agents/overview">
    Run Codex, Claude Code, Amp, OpenCode, or custom agents in isolated workspaces.
  </Card>
</CardGroup>
