Using GitHub Actions to Run Agent SDKs

When thinking about the future of work, I’ve always wondered about those small fixes that you may have always wanted to get done but never had the time or focus to get started. With agent harnesses from Google, OpenAI, and Anthropic becoming more prevalent, tackling these small problems has never been easier.

Popular Agent SDKs:

Why run Agent SDKs in CI/CD?

Typically, we think of agents as local desktop assistants or web-based chatbots. However, embedding them directly into your GitHub repository’s lifecycle unlocks a whole new level of automation:

  1. Event-Driven Autonomy: The agent runs automatically when repository events occur (e.g., when an issue is opened, a comment is posted, or a pull request is submitted).

  2. “Serverless” Execution: There is no need to maintain a persistent running server. GitHub Actions spins up a runner on-demand, executes the agent script, and spins it down.

  3. Deep Context Integration: The agent runs directly inside the checked-out codebase. It can read local configurations, run compiler tests, check lints, and make edits in a completely sandbox-safe environment.

A Real-World Blueprint: The LookML Auto-Fixer

To see how this works in practice, let’s look at a concrete implementation. I recently vibe coded an automation that fixes LookML issues submitted by users directly from GitHub Issues.

Here is the architectural workflow:

Step 1: The GitHub Action Workflow

The YAML file listens for issue comments, authenticates as a GitHub App using a private key and App ID, and configures the environment. It checks out the repository using the App’s token and configures Git to attribute commits to the bot.

name: Agentic LookML Fixer
on:
  issue_comment:
    types: [created]

jobs:
  fix-lookml:
    # Trigger only if comment mentions @looker-agent and is NOT from a bot
    if: |
      github.event.comment.user.login != 'github-actions[bot]' &&
      github.event.comment.user.login != 'looker-agent[bot]' &&
      contains(github.event.comment.body, '@looker-agent')
    runs-on: ubuntu-latest
    steps:
      - name: Generate GitHub App Token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.LOOKER_AGENT_APP_ID }}
          private-key: ${{ secrets.LOOKER_AGENT_APP_KEY }}

      - name: Checkout repository using the App Token
        uses: actions/checkout@v4
        with:
          token: ${{ steps.app-token.outputs.token }}

      # [Python setup, dependency install, and Git config user name/email steps...]

      - name: Run Agent Script
        env:
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          LOOKERSDK_BASE_URL: ${{ secrets.LOOKERSDK_BASE_URL }}
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
        run: python .github/scripts/agentic_fixer.py

      # [Pull Request creation step...]

Step 2: The Agentic Python Script

Using the Antigravity SDK, the Python script starts a stateful agent session. The agent is granted capabilities to write files locally. It reads the issue conversation history, edits the LookML files, and then runs the Looker API compiler checks.

If validation fails, we loop and feed the validation errors back into the agent so it can recursively attempt to fix its own code.

from google.antigravity import Agent, LocalAgentConfig, CapabilitiesConfig

async def main():
    config = LocalAgentConfig(
        system_instructions="You are an expert LookML developer. Fix the files based on the issue thread.",
        capabilities=CapabilitiesConfig() # Grants file write tools to the agent
    )
    
    async with Agent(config) as agent:
        for attempt in range(1, 4):
            # Prompt the agent with the issue context & previous compilation errors
            response = await agent.chat(prompt)
            explanation = await response.text()
            
            # Run compiler/linter check on Looker
            errors = validate_in_looker(sdk, project_id)
            if not errors:
                print("Success! Validation passed.")
                break

Best Practices for CI/CD Agents

When setting up agents in your code repository, follow these governance rules:

  • Trigger Mentions vs Auto-Runs: Use comment-mention triggers (@looker-agent) instead of automatically running the workflow when issues are opened. This gives developers granular control over when to hand over tasks to the agent.

  • Integrate Custom Bot Profiles: Rather than using the default GITHUB_TOKEN (which lists all actions as generic github-actions commits), create a custom GitHub App and generate a token. This provides clean bot-branded comments, PRs, and commit histories.

  • Sandbox Boundaries: Restrict write permissions. Workflows should perform their work on isolated branches (agent-fix/issue-ID) rather than pushing directly to main.

  • Human-in-the-loop Gatekeeping: Always end the workflow by opening a Pull Request. This forces a human developer to review, test, and merge the code before it hits production.

Integrating Agent SDKs with GitHub Actions transforms static issue tracking into an active, collaborative coding loop. Instead of writing simple reminder notifications, your repository can now self heal, fix its own bugs, validate its compilation, and queue up its own code reviews, leaving developers free to focus on building the next big thing.

To get started on your own GitHub Repo, perform the following steps:

  1. Register a new GitHub App named “Looker Agent” in your account’s Developer Settings.

  2. Grant the app Read and write repository permissions for Contents, Issues, and Pull requests under "Permissions & events”.

  3. Download the generated private key .pem file and install the app specifically on your repository.

  4. Store your App’s ID and the entire contents of the private key .pem file in your repository’s secrets as LOOKER_AGENT_APP_ID and LOOKER_AGENT_APP_KEY.

  5. Generate API3 credentials (Client ID and Client Secret) for a service account user in your Looker instance under Admin > Users.

  6. Add your Gemini API key and Looker API credentials to your repository secrets as GEMINI_API_KEY, GCP_PROJECT_ID, LOOKERSDK_BASE_URL, LOOKERSDK_CLIENT_ID, and LOOKERSDK_CLIENT_SECRET.

  7. Commit the workflow file .github/workflows/agentic_lookml_fixer.yml and the runner script .github/scripts/agentic_fixer.py to your default branch.

agentic_lookml_fixer.yml
name: Agentic LookML Fixer

on:
  issue_comment:
    types: [created]

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

jobs:
  fix-lookml:
    if: |
      github.event.comment.user.login != 'github-actions[bot]' &&
      github.event.comment.user.login != 'looker-agent[bot]' &&
      contains(github.event.comment.body, '@looker-agent')
    runs-on: ubuntu-latest
    steps:
      - name: Generate GitHub App Token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.LOOKER_AGENT_APP_ID }}
          private-key: ${{ secrets.LOOKER_AGENT_APP_KEY }}

      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          token: ${{ steps.app-token.outputs.token }}

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'

      - name: Install dependencies
        run: |
          pip install looker_sdk google-antigravity

      - name: Configure Git
        run: |
          git config --global user.name "looker-agent[bot]"
          git config --global user.email "${{ secrets.LOOKER_AGENT_APP_ID }}+looker-agent[bot]@users.noreply.github.com"

      - name: Create or checkout branch
        run: |
          git fetch origin
          BRANCH_NAME="agent-fix/issue-${{ github.event.issue.number }}"
          if git show-ref --verify --quiet refs/remotes/origin/$BRANCH_NAME; then
            git checkout $BRANCH_NAME
            git pull origin $BRANCH_NAME
          else
            git checkout -b $BRANCH_NAME
          fi

      - name: Run Agent Script
        env:
          ISSUE_TITLE: ${{ github.event.issue.title }}
          ISSUE_BODY: ${{ github.event.issue.body }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
          GOOGLE_CLOUD_PROJECT: ${{ secrets.GCP_PROJECT_ID }}
          LOOKERSDK_BASE_URL: ${{ secrets.LOOKERSDK_BASE_URL }}
          LOOKERSDK_CLIENT_ID: ${{ secrets.LOOKERSDK_CLIENT_ID }}
          LOOKERSDK_CLIENT_SECRET: ${{ secrets.LOOKERSDK_CLIENT_SECRET }}
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
        run: python .github/scripts/agentic_fixer.py

      - name: Open Pull Request
        if: success()
        env:
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
        run: |
          BRANCH_NAME="agent-fix/issue-${{ github.event.issue.number }}"
          if gh pr list --head "$BRANCH_NAME" --base master --json number | grep -q '"number":'; then
            echo "PR already exists for branch $BRANCH_NAME. Skipping PR creation."
          else
            gh pr create \
              --title "Fixes #${{ github.event.issue.number }}: ${{ github.event.issue.title }}" \
              --body "This PR was generated autonomously by the AGY SDK agent. It has successfully passed LookML validation." \
              --head "$BRANCH_NAME" \
              --base master
          fi
agentic_fixer.py
import os
import sys
import json
import asyncio
import subprocess
import looker_sdk
from looker_sdk import models40 as models
from google.antigravity import Agent, LocalAgentConfig, CapabilitiesConfig

MAX_ATTEMPTS = 3

def get_issue_thread(issue_number):
    """Fetches the complete issue conversation (description + comments) using gh CLI."""
    try:
        result = subprocess.run(
            ["gh", "issue", "view", str(issue_number), "--json", "title,body,comments"],
            capture_output=True,
            text=True,
            check=True
        )
        data = json.loads(result.stdout)
        title = data.get("title", "")
        body = data.get("body", "")
        comments = data.get("comments", [])
        
        thread = f"Issue Title: {title}\n\nOriginal Issue Description:\n{body}\n"
        
        if comments:
            thread += "\n--- Conversation History ---\n"
            for comment in comments:
                author = comment.get("author", {}).get("login", "unknown")
                comment_body = comment.get("body", "")
                if author == "github-actions[bot]":
                    thread += f"Agent: {comment_body}\n\n"
                else:
                    thread += f"User ({author}): {comment_body}\n\n"
        return thread
    except Exception as e:
        print(f"Failed to fetch issue thread via gh CLI: {e}")
        title = os.environ.get("ISSUE_TITLE", "")
        body = os.environ.get("ISSUE_BODY", "")
        return f"Issue Title: {title}\n\nIssue Body: {body}"

def post_issue_comment(issue_number, message):
    """Posts a comment back to the GitHub issue."""
    print(f"Attempting to post comment for issue {issue_number}...")
    try:
        print(f"Message length: {len(message)} characters.")
        result = subprocess.run(
            ["gh", "issue", "comment", str(issue_number), "--body", message],
            capture_output=True,
            text=True,
            check=True
        )
        print("Successfully posted comment to GitHub Issue.")
        print(f"gh output: {result.stdout.strip()}")
    except subprocess.CalledProcessError as e:
        print(f"Failed to post comment to GitHub Issue. gh CLI failed with exit code {e.returncode}.")
        print(f"gh stdout: {e.stdout.strip()}")
        print(f"gh stderr: {e.stderr.strip()}")
    except Exception as e:
        print(f"Failed to post comment to GitHub Issue due to unexpected exception: {e}")

def sync_with_remote(branch_name, message):
    """Commits local changes and pushes to the remote branch."""
    try:
        subprocess.run(["git", "add", "."], check=True)
        # If there are no changes, git commit will return a non-zero exit code
        result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True)
        if not result.stdout.strip():
            print("No changes to commit.")
            return False
        
        subprocess.run(["git", "commit", "-m", message], check=True)
        # Force push in case of multiple attempts
        subprocess.run(["git", "push", "-u", "origin", branch_name, "--force"], check=True)
        return True
    except subprocess.CalledProcessError as e:
        print(f"Git operation failed: {e}")
        return False

def validate_in_looker(sdk: looker_sdk.methods40.Looker40SDK, project_id: str, branch_name: str):
    """Validates the project in Looker after syncing with the remote branch."""
    print("Switching Looker to dev workspace...")
    sdk.update_session(models.WriteApiSession(workspace_id="dev"))
    
    print(f"Switching Looker to branch {branch_name}...")
    sdk.update_git_branch(project_id, models.WriteGitBranch(name=branch_name))
    
    print("Resetting Looker dev project to remote to ensure latest changes are applied...")
    sdk.reset_project_to_remote(project_id)
    
    print("Running LookML validation...")
    validation_results = sdk.validate_project(project_id)
    
    errors = [e for e in validation_results.errors if e.severity in ["fatal", "error"]]
    return errors

async def main():
    project_id = "demo_cg5839"
    issue_number = os.environ.get("ISSUE_NUMBER", "0")
    branch_name = f"agent-fix/issue-{issue_number}"
    
    print(f"Fetching conversation thread for Issue #{issue_number}...")
    issue_context = get_issue_thread(issue_number)
    
    # Initialize Looker SDK (It automatically reads LOOKERSDK_* env vars)
    print("Initializing Looker SDK...")
    sdk = looker_sdk.init40()
    
    # Initialize AGY Agent Config
    print("Initializing AGY SDK...")
    config = LocalAgentConfig(
        system_instructions=(
            "You are an expert LookML developer and architecture expert. "
            "Your task is to fix LookML files based on a GitHub issue report. "
            "You have full capabilities to read and write files locally. "
            "Read the necessary .lkml files, apply your fixes, and respond with a summary of your changes."
        ),
        capabilities=CapabilitiesConfig()  # Enables tool calling (read_file, write_file, etc.)
    )
    
    validation_errors = []
    agent_explanation = ""
    
    async with Agent(config) as agent:
        for attempt in range(1, MAX_ATTEMPTS + 1):
            print(f"--- Attempt {attempt} of {MAX_ATTEMPTS} ---")
            
            prompt = (
                f"Please fix the following issue.\n\n{issue_context}\n\n"
            )
            
            if validation_errors:
                prompt += "Your previous changes resulted in the following LookML validation errors:\n"
                for err in validation_errors:
                    prompt += f"- [{err.file_path}] {err.message}\n"
                prompt += "\nPlease correct these compilation errors in the LookML files."
            else:
                prompt += "Please identify the relevant LookML files in this repository, edit them directly using your tools to fix the issue, and then respond with a summary of the changes you made."
                
            print("Prompting agent to apply fixes...")
            response = await agent.chat(prompt)
            agent_explanation = await response.text()
            print(f"Agent response:\n{agent_explanation}\n")
            
            print("Syncing changes with remote repository...")
            commit_message = f"Agent fix attempt {attempt} for issue #{issue_number}"
            sync_with_remote(branch_name, commit_message)
            
            print("Running Looker validation...")
            validation_errors = validate_in_looker(sdk, project_id, branch_name)
            
            if not validation_errors:
                print("LookML validation passed successfully!")
                comment_body = (
                    f"### Agent Fix Success (Attempt {attempt}/{MAX_ATTEMPTS})\n\n"
                    f"LookML validation passed successfully!\n\n"
                    f"**Changes made:**\n{agent_explanation}\n\n"
                    f"The changes have been pushed to the PR branch."
                )
                post_issue_comment(issue_number, comment_body)
                sys.exit(0)
            else:
                print(f"Validation failed with {len(validation_errors)} errors.")
                for err in validation_errors:
                    print(f" - {err.message} (File: {err.file_path})")
                    
        comment_body = (
            f"### Agent Fix Failed\n\n"
            f"I attempted to fix the LookML files {MAX_ATTEMPTS} times, but validation still failed. "
            f"Here are the remaining validation errors:\n\n"
        )
        for err in validation_errors:
            comment_body += f"- **[{err.file_path}]**: {err.message}\n"
        comment_body += "\nLast changes made:\n" + agent_explanation
        post_issue_comment(issue_number, comment_body)
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(main())
  1. Start opening issues and put your agents to work by adding a comment including @looker-agent
2 Likes