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:
- Anthropic’s Agent SDK
- OpenAI’s Codex SDK
- Google’s Antigravity SDK
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:
-
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).
-
“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.
-
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 tomain. -
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:
-
Register a new GitHub App named “Looker Agent” in your account’s Developer Settings.
-
Grant the app Read and write repository permissions for Contents, Issues, and Pull requests under "Permissions & events”.
-
Download the generated private key
.pemfile and install the app specifically on your repository. -
Store your App’s ID and the entire contents of the private key
.pemfile in your repository’s secrets asLOOKER_AGENT_APP_IDandLOOKER_AGENT_APP_KEY. -
Generate API3 credentials (Client ID and Client Secret) for a service account user in your Looker instance under Admin > Users.
-
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, andLOOKERSDK_CLIENT_SECRET. -
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())
- Start opening issues and put your agents to work by adding a comment including @looker-agent
![flowchart LR A["👤 <b>User Trigger</b><br>Comments '@looker-agent'"] --> B["⚙️ <b>Action Setup</b><br>• Trigger GitHub Action<br>• Generate App Token<br>• Checkout branch<br>• Configure Git Bot"] B --> C["🤖 <b>Agent Workflow</b><br>• Run Fixer script<br>• Edit LookML files<br>• Sync changes"] C --> D{"Looker<br>Validator"} D -- "Fail" --> E["Feed errors<br>back to Agent"] E --> C D -- "Pass" --> F["✅ <b>Completion</b><br>• Post success comment<br>• Open/Update PR"]](https://d2yds90mtvelsl.cloudfront.net/optimized/4X/3/f/6/3f6c0fb606d8dd2a4bba9a13dca888fe64caf992_2_690x128.jpeg)