CI/CD Pipeline Patterns
PLCI/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.jsonandDockerfileinto 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:
claude --permission-mode planPaste the prompt below. When the plan names the scripts it found in package.json, approve it and let Claude Code write the file.
From the repository root, run Codex with the workspace permission profile (beta in Codex CLI 0.157.1), which lets it write inside the repository and nowhere else:
codex -c default_permissions=":workspace"Paste the prompt below. The :workspace profile grants no network access, so Codex asks before it runs git ls-remote to look up action SHAs; approve that one request.
Open the repository in Cursor, switch the agent to Plan Mode, and add @package.json, @Dockerfile, and any existing workflow files to the context. Paste the prompt below, review the plan, then let the agent write .github/workflows/ci.yml and review the diff in the editor before you keep it.
A good result has this shape. The excerpt shows tags for readability; the agent’s output carries SHAs:
# .github/workflows/ci.yml (excerpt)name: CIon: 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 rolloutThree 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: truebefore the scan,docker pushafter it means a vulnerable image never reaches the registry. Scanning afterpush: trueblocks the deploy, but the bad image is already published.environment: productiondoes 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.
-
Lint the workflow syntax and expressions.
actionlintcatches invalid keys, broken${{ }}expressions, and shell errors inrun:blocks. Install it withbrew install actionlintorgo install github.com/rhysd/actionlint/cmd/actionlint@latest, then runactionlintfrom the repository root. -
Audit the workflow for security defects.
zizmor(PyPIzizmor1.30.1, checked 2026-09-26) flagstemplate-injection,excessive-permissions,unpinned-uses,cache-poisoning, andartipacked(checkout credentials left on disk). Runuvx zizmor .github/workflows/. Since v1.20.0 its default policy requires every action to be pinned by SHA, includingactions/*, so a tag-pinned workflow fails this step until the agent resolves the SHAs. -
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:
imageneedstest, 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, anddeploy-prodmust never start. -
Make the gates binding. In the branch protection or ruleset for
main, marktestandimageas required status checks, and add required reviewers to theproductionenvironment. 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/underCODEOWNERSand turn on required code-owner review in the same ruleset. -
Record who signs off. The required reviewers on
productionare 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:
gh run view RUN_ID --log-failed > failed.logRUN_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:
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.
codex mcp add github-actions \ --url https://api.githubcopilot.com/mcp/x/actions/readonly \ --bearer-token-env-var GITHUB_PATCodex reads the variable at connection time and never writes the token to config.toml.
In ~/.cursor/mcp.json, the user-global file that never gets committed:
{ "mcpServers": { "github-actions": { "url": "https://api.githubcopilot.com/mcp/x/actions/readonly", "headers": { "Authorization": "Bearer ${env:GITHUB_PAT}" } } }}This follows the shape in GitHub’s Cursor install guide; the ${env:NAME} interpolation is from Cursor’s MCP docs as quoted in our research, not tested in a Cursor binary. Confirm in Settings > MCP that github-actions lists tools, and see the Cursor tab of Install the GitHub MCP server for the full setup.
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.
What changes for GitLab CI and Jenkins?
Section titled “What changes for GitLab CI and Jenkins?”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.ymlandJobs/Container-Scanning.gitlab-ci.yml, instead of hand-rolled scanner jobs. The olderSecurity/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 asnode:22-alpine, and key the cache on the lockfile withcache: key: files: [package-lock.json]. - Jenkins: ask for a declarative
Jenkinsfilewith Docker agents andparallelquality stages, and a SonarQube stage that callswithSonarQubeEnvfollowed bywaitForQualityGate abortPipeline: true. WithoutabortPipeline: 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:
name: Claude pipeline reviewon: 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 tokenjobs: 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.
openai/codex-action@v1 installs the Codex CLI and runs codex exec. Run Codex in a job that can only read, and post its answer from a second job that holds the write permission:
name: Codex pipeline reviewon: pull_request: types: [opened, synchronize, reopened] paths: ['.github/workflows/**', 'Dockerfile', '.gitlab-ci.yml', 'Jenkinsfile']jobs: codex: runs-on: ubuntu-latest permissions: contents: read outputs: final_message: ${{ steps.run_codex.outputs.final-message }} steps: - uses: actions/checkout@v7 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge fetch-depth: 0 # base and head SHAs reachable for the diff persist-credentials: false - id: run_codex uses: openai/codex-action@v1 with: openai-api-key: ${{ secrets.OPENAI_API_KEY }} permission-profile: ':read-only' prompt: | Review only the CI/CD changes between ${{ github.event.pull_request.base.sha }} and ${{ github.event.pull_request.head.sha }}. Flag gates that can no longer fail, widened permissions, unpinned actions, event data used inside run: scripts, and deploy conditions broader than releases. Cite file and line for each finding. No style comments. post: needs: codex if: needs.codex.outputs.final_message != '' runs-on: ubuntu-latest permissions: pull-requests: write steps: - env: GH_TOKEN: ${{ github.token }} BODY: ${{ needs.codex.outputs.final_message }} PR_NUMBER: ${{ github.event.pull_request.number }} run: gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body "$BODY"Keep the default safety-strategy: drop-sudo, which removes sudo before Codex runs. OpenAI’s action README prefers a permission profile over the legacy sandbox input for new workflows, and the two inputs cannot be combined. Leave model empty to use Codex’s default model; the models hub lists what that is today. The codex job reads the pull request’s own files, including any AGENTS.md or .codex/ it adds, with OPENAI_API_KEY in scope, so :read-only and the missing write permission are what keep a hostile pull request harmless: never give that job write permissions or write tools. As with the Claude Code job, anyone who can push a branch can edit this workflow before it runs, so the review stays advisory.
Cursor’s pull request reviewer is Bugbot, which reviews pull requests for bugs, security issues, and code quality problems once you enable it for the repository (Cursor docs, checked 2026-08-28; setup on the Bugbot page). Put the same checklist as the Claude Code prompt in .cursor/BUGBOT.md, the project rules file Bugbot reads: gates that can no longer fail, widened permissions, unpinned actions, and broad deploy conditions. For a scripted step inside the workflow itself, Cursor documents running its CLI in GitHub Actions (Cursor docs, checked 2026-08-28). A common split is to author the pipeline in the editor and let Bugbot or one of the other two actions review it in CI.
When generated CI/CD pipelines break
Section titled “When generated CI/CD pipelines break”-
The cache restores, yet the build breaks or recompiles everything. The key is missing the lockfile hash or the Node version, so
node_modulesbuilt for one Node version is restored under another. Useactions/setup-nodewithcache: npm, which caches the npm download cache rather thannode_modules. If you needactions/cache, key it on${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}. -
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. -
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: productionsends the subjectrepo: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 itssubcondition with the claim in the failed run. -
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: trueskips vulnerabilities with no fixed version. For the rest, add the CVE to.trivyignorewith an expiry, for exampleCVE-2026-12345 exp:2026-12-31, and a comment naming the reviewer. Trivy re-raises it when the date passes. -
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 withgit ls-remote, tag as a comment”, and letzizmor’sunpinned-usesaudit confirm it. -
The AI review job never comments. Check three things in order: the pull request came from a fork (no secrets, by design); the
pathsfilter excluded the changed file; or the job lackspull-requests: write. Fix the filter or the permission. Do not widen the trigger.
Where to go next with CI/CD pipelines
Section titled “Where to go next with CI/CD pipelines”Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in