Skip to content

CI/CD pipeline recipes for AI agents are copy-paste prompts and reviewed YAML that make Claude Code, Codex, or Cursor generate GitHub Actions, GitLab CI, or Jenkins pipelines whose gates can fail: tests from the repository’s real scripts, an image scan that blocks the push, actions pinned to commits, and a deploy that waits for a human approver.

Your deploy passed every check and still shipped a critical CVE. The tests were green and the image built, but the scan step only uploaded a report, so nothing in the pipeline could say no. The agent that wrote the workflow did exactly what it was asked; nobody asked it for a gate that fails.

This page is for the developer or tech lead who owns a service’s pipeline and wants the agent to write it. The recipes below produce the YAML, and then prove each gate can turn red, so you trust the pipeline without re-reading 200 lines of it.

What you’ll walk away with from these CI/CD recipes

Section titled “What you’ll walk away with from these CI/CD recipes”
  • A prompt that turns package.json and Dockerfile into a GitHub Actions workflow with a Node matrix, a Trivy scan that runs before the push, and a deploy job behind a GitHub Environment.
  • A gate drill: actionlint, zizmor, and throwaway pull requests that must go red, so you know each gate blocks instead of assuming it does.
  • A prompt that turns a failed run’s log into one root cause and one fix.
  • An AI review step for pipeline pull requests with Claude Code, Codex, or Cursor, configured with least privilege.
  • The failure modes generated pipelines hit first, with the recovery for each.

The broader pipeline techniques (change-aware builds, flaky-test quarantine, progressive rollouts) are on Pipeline automation with AI. This page stays on generating and hardening the pipeline file itself.

How do you get an agent to generate a pipeline that matches your repo?

Section titled “How do you get an agent to generate a pipeline that matches your repo?”

A bare “create a CI/CD pipeline” prompt produces a plausible workflow for a project you do not have: the wrong package manager, an invented npm run test:ci, a Node version you never ran. Anchor the agent to the files that already exist, and name the gates you want in terms of what makes them fail.

From the repository root, run claude. Claude Code reads package.json, the Dockerfile, and any existing .github/workflows/* itself, so you paste nothing. To review the plan before any file changes, start in plan mode:

Terminal window
claude --permission-mode plan

Paste the prompt below. When the plan names the scripts it found in package.json, approve it and let Claude Code write the file.

A good result has this shape. The excerpt shows tags for readability; the agent’s output carries SHAs:

# .github/workflows/ci.yml (excerpt)
name: CI
on:
push: { branches: [main] }
pull_request:
release: { types: [published] }
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: { node: [22, 24] }
steps:
- uses: actions/checkout@v7
with: { persist-credentials: false }
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test # the script that exists in package.json
image:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/acme/billing-api:${{ github.sha }} # GHCR names must be lowercase
steps:
- uses: actions/checkout@v7
with: { persist-credentials: false }
- uses: docker/setup-buildx-action@v4
- name: Build (loaded locally, not pushed)
uses: docker/build-push-action@v7
with:
load: true
tags: ${{ env.IMAGE }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan - fails the job on fixable CRITICAL/HIGH
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: ${{ env.IMAGE }}
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
- if: github.event_name != 'pull_request'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push only what passed the scan
if: github.event_name != 'pull_request'
run: docker push "$IMAGE"
deploy-prod:
needs: image
if: github.event_name == 'release'
runs-on: ubuntu-latest
environment: production # required reviewers on this environment = the human sign-off
permissions:
contents: read
id-token: write # OIDC to the cloud, no stored cloud keys
steps:
- uses: actions/checkout@v7
with: { persist-credentials: false }
# TODO: cloud login via OIDC, then the rollout

Three lines carry the safety, so they are the three you check first:

  • exit-code: '1' turns the scan from a report into a gate. Without it, Trivy prints findings and the job stays green.
  • load: true before the scan, docker push after it means a vulnerable image never reaches the registry. Scanning after push: true blocks the deploy, but the bad image is already published.
  • environment: production does nothing until you add required reviewers to that environment in the repository settings. The YAML names the gate; the setting enforces it.

For the Dockerfile the pipeline builds, see Docker and container patterns; for the rollout the deploy job runs, see Kubernetes orchestration patterns and progressive delivery for agent-written changes.

How do you prove the pipeline’s gates actually block?

Section titled “How do you prove the pipeline’s gates actually block?”

A generated pipeline that has never gone red is an untested pipeline. Instead of reading every line, run two static checkers and one drill that makes each gate fail on purpose. The same rule applies to tests: a gate that cannot fail is worse than no gate, because it earns the green check without the safety.

  1. Lint the workflow syntax and expressions. actionlint catches invalid keys, broken ${{ }} expressions, and shell errors in run: blocks. Install it with brew install actionlint or go install github.com/rhysd/actionlint/cmd/actionlint@latest, then run actionlint from the repository root.

  2. Audit the workflow for security defects. zizmor (PyPI zizmor 1.30.1, checked 2026-09-26) flags template-injection, excessive-permissions, unpinned-uses, cache-poisoning, and artipacked (checkout credentials left on disk). Run uvx zizmor .github/workflows/. Since v1.20.0 its default policy requires every action to be pinned by SHA, including actions/*, so a tag-pinned workflow fails this step until the agent resolves the SHAs.

  3. Run the gate drill. Have the agent open draft pull requests from throwaway branches, each carrying exactly one deliberate failure (the prompt below). Keep the failures on separate branches: image needs test, so a failing test on the same branch would skip the scan and prove nothing about it. Each gate must go red on its own branch, and deploy-prod must never start.

  4. Make the gates binding. In the branch protection or ruleset for main, mark test and image as required status checks, and add required reviewers to the production environment. Until then a red check is advice: anyone with merge rights can merge past it. Required checks still run the workflow file from the pull request itself, so a pull request can delete the scan step and stay green; put /.github/ under CODEOWNERS and turn on required code-owner review in the same ruleset.

  5. Record who signs off. The required reviewers on production are the people with production authority. The agent writes and fixes the pipeline; a named human approves each production deploy. See production approval as a system boundary.

When the drill comes back, compare the actual red checks with the agent’s list. A gate that stayed green on its branch is a finding, not a flake. Close the draft pull requests and delete the branches once every gate has failed where it should.

How do you diagnose a red run without scrolling the log?

Section titled “How do you diagnose a red run without scrolling the log?”

The expensive half of CI is the run that fails 400 lines into a log with npm ci exiting 1. Fetch only the failed steps with the GitHub CLI and give the agent the log together with the job definition:

Terminal window
gh run view RUN_ID --log-failed > failed.log

RUN_ID is the number in the run’s URL. The workflow is identical in all three tools: in Claude Code and Codex, reference failed.log by path in the prompt; in Cursor, attach it with @failed.log.

To let the agent read runs itself, register the GitHub MCP server’s read-only Actions toolset with a fine-grained, read-only personal access token in GITHUB_PAT:

Terminal window
claude mcp add --transport http github-actions \
https://api.githubcopilot.com/mcp/x/actions/readonly \
-H "Authorization: Bearer $GITHUB_PAT"

The shell expands $GITHUB_PAT once, and Claude Code stores the resulting value in its configuration, so use a token scoped to read Actions only.

More on the server’s toolsets and lockdown mode is on the GitHub MCP server page.

The “in CI but not locally” question steers the model toward environment causes: a cache key restoring dependencies built for another Node version, a lockfile out of sync with package.json, a missing service container, or a token without the permission the step needs. Point 4 is the verification: the fix is done when that check turns green, not when the explanation sounds right.

The generate-then-drill loop is the same; only the target file and the gate syntax change. Swap .github/workflows/ci.yml in the prompts for .gitlab-ci.yml or a declarative Jenkinsfile, and add these platform notes to the prompt:

  • GitLab CI: ask for GitLab’s maintained templates, include: - template: Jobs/SAST.gitlab-ci.yml and Jobs/Container-Scanning.gitlab-ci.yml, instead of hand-rolled scanner jobs. The older Security/ paths are redirect stubs (Container Scanning moved in GitLab 15.6), and generated pipelines often still emit them. Pin job images to a real tag such as node:22-alpine, and key the cache on the lockfile with cache: key: files: [package-lock.json].
  • Jenkins: ask for a declarative Jenkinsfile with Docker agents and parallel quality stages, and a SonarQube stage that calls withSonarQubeEnv followed by waitForQualityGate abortPipeline: true. Without abortPipeline: true, a failed quality gate is reported and the pipeline continues.

How do you add an AI review step to pipeline pull requests?

Section titled “How do you add an AI review step to pipeline pull requests?”

An AI reviewer on pipeline changes catches what the checkers cannot: a deploy condition that is too broad, a permission widened “temporarily”, a scan moved after the push. It is advisory. The required checks and the environment approval stay the gates, and a review comment never replaces them.

Run /install-github-app inside claude to install the Claude GitHub app and store ANTHROPIC_API_KEY as a repository secret, then add a review workflow scoped to pipeline files:

.github/workflows/claude-pipeline-review.yml
name: Claude pipeline review
on:
pull_request:
types: [opened, synchronize]
paths: ['.github/workflows/**', 'Dockerfile', '.gitlab-ci.yml', 'Jenkinsfile']
permissions:
contents: read
pull-requests: write
id-token: write # the action swaps an OIDC token for the Claude GitHub App token
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with: { persist-credentials: false, fetch-depth: 1 }
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Review only the CI/CD changes in this pull request. Flag: gates
that can no longer fail, widened permissions, unpinned actions,
event data used inside run: scripts, secrets exposed to pull
request code, and deploy conditions broader than releases.
Comment inline on the exact line. Do not suggest style changes.
claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*)"'

id-token: write is required with the default GitHub App authentication: without it the action cannot get the OIDC token it exchanges for the app token, and the job fails before it reviews anything. If you would rather not use the app, pass github_token: ${{ secrets.GITHUB_TOKEN }} instead, and comments then come from github-actions[bot]. --allowedTools limits the reviewer to reading the diff and posting inline comments. On pull requests, the action restores .claude/, .mcp.json, and CLAUDE.md from the base branch before Claude starts, so a pull request cannot swap in its own hooks or MCP servers. The action configures its own git credentials before that fetch, so persist-credentials: false on the checkout does not break the restore (claude-code-action source, checked 2026-09-26). That restore protects the agent’s configuration, not this workflow: on pull_request GitHub runs the YAML from the pull request, and the paths filter includes .github/workflows/**, so anyone who can push a branch to this repository can widen --allowedTools in their own copy of the job, and it runs with ANTHROPIC_API_KEY before any human review. CODEOWNERS on /.github/ with required review stops such a change from merging, not from running, so give only trusted people push access and treat the AI review as advisory.

  1. The cache restores, yet the build breaks or recompiles everything. The key is missing the lockfile hash or the Node version, so node_modules built for one Node version is restored under another. Use actions/setup-node with cache: npm, which caches the npm download cache rather than node_modules. If you need actions/cache, key it on ${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}.

  2. The push to GHCR fails with “repository name must be lowercase”. ${{ github.repository }} keeps the owner’s capitalisation. Lowercase the name in a prior step, or write it out in lowercase as the skeleton does, and re-run.

  3. The OIDC deploy fails with “Not authorized to perform sts:AssumeRoleWithWebIdentity”. The YAML is usually fine; the cloud trust policy does not match the token. A job that uses environment: production sends the subject repo:ORG/REPO:environment:production, not a branch ref. Ask the agent to generate the trust policy from the workflow’s trigger and environment, then compare its sub condition with the claim in the failed run.

  4. Trivy fails on a CVE that has no fix yet. Do not delete or soften the scan step; a scan that cannot fail is the opening incident all over again. ignore-unfixed: true skips vulnerabilities with no fixed version. For the rest, add the CVE to .trivyignore with an expiry, for example CVE-2026-12345 exp:2026-12-31, and a comment naming the reviewer. Trivy re-raises it when the date passes.

  5. The agent pins actions to tags, @main, or versions that do not exist. Agents reproduce the versions from their training data and guess SHAs. The malicious LiteLLM 1.82.7/1.82.8 releases on PyPI (2026-03-24) were traced to a compromised Trivy step in LiteLLM’s CI (secondary reports; tracking issue BerriAI/litellm#24518). Pinning by SHA means a moved or hijacked tag cannot change what your pipeline runs. Re-prompt with “resolve every action to its full commit SHA with git ls-remote, tag as a comment”, and let zizmor’s unpinned-uses audit confirm it.

  6. The AI review job never comments. Check three things in order: the pull request came from a fork (no secrets, by design); the paths filter excluded the changed file; or the job lacks pull-requests: write. Fix the filter or the permission. Do not widen the trigger.

Did this guide help you?

One tap, no sign-in.

not at all a lot