CI/CD Pipeline Generator: GitHub Actions Prompts and YAML
What is AI-assisted CI/CD pipeline generation?
AI-assisted CI/CD pipeline generation uses an LLM to draft GitHub Actions YAML from a prompt that defines the stack, triggers, stages, deployment target, and constraints. The draft can save setup time, but it still needs a security and correctness review before it is committed.
TL;DR
- -A useful CI/CD prompt covers 5 elements: stack and versions, triggers, stages, infrastructure details, and constraints such as timeouts and concurrency.
- -Review every generated workflow for 4 security patterns: full-SHA action pins, minimal token permissions, narrow secret scope, and masked sensitive output.
- -A four-pass review (base → security → optimization → extension) makes failures easier to spot than a single oversized prompt.
- -Reusable workflows via workflow_call eliminate config duplication across repositories - one update propagates to all projects in the organization.
- -Without timeout-minutes on every job, a hung GitHub Actions runner can block for up to 6 hours (the platform default limit).
The dangerous part of an AI-generated workflow is rarely invalid YAML. It is the
missing permissions block, the six-hour timeout, or a third-party action referenced
by a mutable tag. A model can draft a workflow in a minute; it cannot infer your trust
boundary unless the prompt states it.
The examples below cover Node.js, Python, Docker, and staged deployment. They use current major action tags to stay readable. Before shipping them, pin every external action to a verified full commit SHA and review the resulting permissions.
Anatomy of a CI/CD Generation Prompt
The more context you give, the more accurate the YAML. This follows the same principle behind context engineering for LLMs - structured input produces structured output. A prompt for GitHub Actions needs five elements:
- Stack and versions - language, runtime, package manager
- Triggers - which events launch the pipeline
- Stages - what to check and in what order
- Infrastructure - where to deploy, which secrets are needed
- Constraints - timeouts, concurrency, skip conditions
Omit one and the model has to guess. Include all five and you get a draft that is specific enough to review against your repository.
Base prompt template:
Generate a GitHub Actions workflow for:
- Stack: [language] [version], [package manager]
- Triggers: push to main, pull_request to main
- Steps: install deps, lint, test, build
- Cache: [package manager] cache
- Node version matrix: [versions]
- Fail fast: true
- Timeout: 15 minutes per job
First Prompt: Node.js with Matrix Testing
Start with the most common case: a TypeScript project that needs linting, tests across multiple Node.js versions, and a build.
Prompt:
Generate a GitHub Actions workflow for a TypeScript project:
- Runtime: Node.js 22, 24, 26 (matrix strategy)
- Package manager: pnpm 11 with caching
- Triggers: push to main, pull_request to main
- Jobs: lint (single node version), test (matrix), build (single, needs test)
- Lint: eslint + prettier check
- Test: vitest with coverage, upload coverage artifact
- Build: tsc --noEmit, then vite build
- Fail fast: false (run all matrix combinations)
- Timeout: 10 minutes per job
- Concurrency: cancel in-progress runs for same PR
The AI produces the following draft. Node 24 is the LTS baseline; Node 22 checks the older supported line, while Node 26 catches compatibility issues against Current. Check the Node.js release schedule before copying the matrix into a long-lived repository.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm prettier --check .
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version: [22, 24, 26]
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --coverage
- uses: actions/upload-artifact@v7
if: matrix.node-version == 24
with:
name: coverage-report
path: coverage/
build:
needs: [lint, test]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm tsc --noEmit
- run: pnpm build
The model got the details right: --frozen-lockfile for reproducibility, pnpm cache through the built-in actions/setup-node option, coverage upload from only one Node.js version, and concurrency to cancel stale runs on new pushes.
Prompt for Python: Tests + Linting + Package Publishing
Python projects need a different structure: virtual environments, pip or poetry for dependencies, different caching behavior. This matrix starts at Python 3.12 and includes 3.14, the current feature series as of this update. Recheck the official Python release list when you refresh the workflow.
Prompt:
Generate GitHub Actions workflow for a Python library:
- Python versions: 3.12, 3.13, 3.14 (matrix)
- Dependency manager: poetry with caching
- Triggers: push to main, pull_request, release published
- Jobs:
1. quality: ruff check + ruff format --check + mypy (single python version)
2. test: pytest with matrix, upload junit xml
3. publish: build wheel, publish to PyPI (only on release event)
- Publish uses trusted publishing (OIDC), no API tokens
- Timeout: 15 minutes
name: CI/CD
on:
push:
branches: [main]
pull_request:
release:
types: [published]
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install poetry
run: pipx install poetry
- name: Cache poetry
uses: actions/cache@v6
with:
path: ~/.cache/pypoetry
key: poetry-${{ hashFiles('poetry.lock') }}
- run: poetry install --no-interaction
- run: poetry run ruff check .
- run: poetry run ruff format --check .
- run: poetry run mypy src/
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: pipx install poetry
- name: Cache poetry
uses: actions/cache@v6
with:
path: ~/.cache/pypoetry
key: poetry-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }}
- run: poetry install --no-interaction
- run: poetry run pytest --junitxml=results.xml
- uses: actions/upload-artifact@v7
if: always()
with:
name: test-results-${{ matrix.python-version }}
path: results.xml
publish:
if: github.event_name == 'release'
needs: [quality, test]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
id-token: write
environment: pypi
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install poetry
run: pipx install poetry
- run: poetry build
- uses: pypa/gh-action-pypi-publish@release/v1
The key detail here: trusted publishing via OIDC. No API tokens stored in secrets - PyPI verifies the release through GitHub’s identity provider. That’s the current standard for open-source packages.
Docker: Multi-Stage Build + Registry Push
Container projects add registry auth, layer caching, and multi-platform builds to the mix.
Prompt:
Generate GitHub Actions for Docker image:
- Triggers: push to main (build+push), pull_request (build only)
- Registry: GitHub Container Registry (ghcr.io)
- Multi-platform: linux/amd64, linux/arm64
- Cache: GitHub Actions cache backend
- Tags: sha, branch name, latest (only on main), semver from git tag
- Scan image with trivy before push
- Timeout: 20 minutes
name: Docker
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
packages: write
security-events: write
steps:
- uses: actions/checkout@v7
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=ref,event=branch
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Scan with Trivy
uses: aquasecurity/[email protected]
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: sarif
output: trivy-results.sarif
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy-results.sarif
docker/metadata-action generates tags from git context automatically. cache-from: type=gha uses the built-in GitHub Actions cache - no external registry needed for layer caching.
Multi-Stage Pipeline: From PR to Production
A full deployment pipeline adds staging and production stages with manual approval. The prompt gets longer here - it has to describe the business logic of the deployment, not just the technical steps.
Prompt:
Generate a multi-stage deployment pipeline:
- Trigger: push to main deploys to staging automatically
- Production deploy: manual workflow_dispatch with environment approval
- Stages: test → build → deploy-staging → deploy-production
- Deploy target: Cloudflare Pages (staging and production)
- Environment secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID
- Build artifact shared between deploy jobs
- Slack notification on deploy success/failure
- Production requires GitHub environment protection rules
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Deploy target"
required: true
type: choice
options:
- staging
- production
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v7
with:
name: build-output
path: dist/
retention-days: 3
deploy-staging:
if: github.event_name == 'push' || inputs.environment == 'staging'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/download-artifact@v8
with:
name: build-output
path: dist/
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/ --project-name=my-project --branch=staging
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v3
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "Staging deploy ${{ job.status }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
deploy-production:
if: inputs.environment == 'production'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment:
name: production
url: https://example.com
steps:
- uses: actions/download-artifact@v8
with:
name: build-output
path: dist/
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist/ --project-name=my-project --branch=main
- name: Notify Slack
if: always()
uses: slackapi/slack-github-action@v3
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": "Production deploy ${{ job.status }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
The build artifact is created once and reused in both deploys. Staging goes out automatically on push to main. Production requires a manual trigger via workflow_dispatch and explicit approval through GitHub environment protection rules.
Best Practices: What AI Misses
AI models generate structurally correct workflows but they miss the same things every time. Here’s what to check when you review a generated pipeline.
Security
Pinning actions by SHA. The standard uses: actions/checkout@v7 references a
mutable tag. GitHub’s secure-use reference
calls a full-length commit SHA the only immutable way to reference an action. Verify
the commit in the official repository before pinning it:
# Instead of this
- uses: actions/checkout@v7
# Use this
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Add to your prompt: Pin all third-party actions to full SHA commit hash with version comment.
Minimal permissions. Defaults vary by repository and organization policy. New personal repositories start with read access to contents and packages, but inherited settings can differ. Declare the token permissions in the workflow instead of relying on an administrator’s current default:
permissions:
contents: read
packages: write
Secrets. The model occasionally suggests hardcoded values or reaches for ${{ secrets.GITHUB_TOKEN }} where you actually need a scoped token. Verify which secrets the workflow uses and why. For a deeper look at AI-generated code security gaps, see the AI security audit checklist.
Performance
Caching. Prefer the cache support in actions/setup-node when a single lockfile
defines the dependency graph. In a monorepo, set cache-dependency-path to the right
lockfile or use actions/cache when you need a custom scope and restore policy.
Parallel jobs. The model tends to chain everything linearly via needs. Lint and test can run in parallel:
build:
needs: [lint, test] # lint and test run in parallel
Conditional steps. Skip expensive steps when only docs changed:
- name: Check for code changes
id: changes
uses: dorny/paths-filter@v3
with:
filters: |
code:
- 'src/**'
- 'package.json'
- name: Run tests
if: steps.changes.outputs.code == 'true'
run: npm test
Reliability
Retry for flaky steps. Network operations - npm install, docker push - fail on timeouts more often than you’d think. Add retry:
- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 3
command: npm ci
Timeouts. Without timeout-minutes, a hung job can hold your runner for up to 6 hours - GitHub’s default limit. Always set an explicit timeout.
Concurrency. Without a concurrency group, two quick pushes launch two identical pipelines. Cancel the previous run for PR checks:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Advanced Prompts: Reusable Workflows and Composite Actions
When you have many projects each carrying a copy of the same CI/CD config, keeping them in sync becomes a real maintenance problem. GitHub solves this with reusable workflows and composite actions.
Prompt for a reusable workflow:
Generate a reusable GitHub Actions workflow (.github/workflows/reusable-node-ci.yml):
- Callable via workflow_call
- Inputs: node-version (string, default "24"), package-manager (string, default "pnpm"), run-lint (boolean, default true)
- Secrets: inherited
- Jobs: install, lint (conditional on input), test, build
- Cache based on package manager input
name: Node.js CI (Reusable)
on:
workflow_call:
inputs:
node-version:
type: string
default: "24"
package-manager:
type: string
default: "pnpm"
run-lint:
type: boolean
default: true
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- if: inputs.package-manager == 'pnpm'
uses: pnpm/action-setup@v4
with:
version: 11
- uses: actions/setup-node@v7
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.package-manager }}
- name: Install (npm)
if: inputs.package-manager == 'npm'
run: npm ci
- name: Install (pnpm)
if: inputs.package-manager == 'pnpm'
run: pnpm install --frozen-lockfile
- name: Lint
if: inputs.run-lint
run: ${{ inputs.package-manager }} run lint
- run: ${{ inputs.package-manager }} test
- run: ${{ inputs.package-manager }} run build
Calling it from another repository:
jobs:
ci:
uses: my-org/.github/.github/workflows/reusable-node-ci.yml@main
with:
node-version: "24"
package-manager: pnpm
run-lint: true
secrets: inherit
Update the config in one place, and it propagates to every project in the organization.
Iterative Refinement: Prompt Chaining
Complex pipelines rarely come out right on the first try. A prompt chain works better:
- Base prompt - generates the workflow skeleton
- Review prompt - “Review this workflow for security issues, missing caches, and unnecessary steps”
- Optimization prompt - “Optimize this workflow to run under 5 minutes by parallelizing jobs”
- Extension prompt - “Add Slack notifications, artifact uploads, and deployment to staging”
Each step refines what came before. The model has the context and makes targeted edits instead of regenerating everything from scratch. If you’re managing many prompts like these across projects, a prompt engineering system helps keep them organized.
Example review prompt:
Review this GitHub Actions workflow for:
1. Security: pinned actions, minimal permissions, secret handling
2. Performance: caching, parallelism, conditional execution
3. Reliability: timeouts, retry, concurrency groups
4. Maintainability: DRY (reusable workflows), clear naming
List issues as: [SEVERITY] description → fix
The model returns a structured list of issues with fixes. This same pattern works for AI-assisted code review - just applied to infrastructure instead of application code.
Protecting Against Cascading Pipeline Failures
A CI/CD pipeline depends on external services: npm registry, Docker Hub, cloud providers. One downed registry can block all deployments. The circuit breaker principles apply directly:
- Fallback registry. Configure
.npmrcwith a backup registry - Retry with backoff. Network steps should retry with increasing delays
- Timeout on every step. Not just on the job - on individual steps via
timeout-minutes - Cache as circuit breaker. If a registry is down, cached dependencies let tests pass while you wait it out (not for deployments, obviously)
Universal Generator Prompt Template
A final prompt covering most scenarios:
Generate a production-ready GitHub Actions workflow:
PROJECT:
- Language: [X], version: [Y]
- Package manager: [Z]
- Monorepo: yes/no
TRIGGERS:
- push: [branches]
- pull_request: [branches]
- release: published
- schedule: [cron]
- workflow_dispatch: [inputs]
JOBS (in dependency order):
1. [job-name]: [description] (runs on: [os], timeout: [min])
2. ...
REQUIREMENTS:
- Pin all third-party actions to SHA
- Minimal permissions per job
- Cache: [strategy]
- Concurrency: cancel in-progress for PRs
- Artifacts: [what to upload]
- Notifications: [Slack/email/none]
- Environments: [staging/production with protection rules]
CONSTRAINTS:
- Total pipeline time: under [X] minutes
- Runner: [ubuntu-latest / self-hosted]
- No secrets in logs (mask sensitive outputs)
Fill in the brackets, then review the output as infrastructure code. If the workflow builds a container, use the Docker multi-stage guide to keep build context, layer order, runtime contents, and image scanning explicit.
Result
Use the model for the first draft, not the final approval. A prompt that names the stack, triggers, stages, infrastructure, and constraints gives reviewers something concrete to inspect. Then verify permissions, secret boundaries, action SHAs, and timeouts before merging. Once that review is repeatable, move the stable parts into a reusable workflow instead of generating the same YAML again.
The useful outcome is not a five-minute demo. It is a reviewable workflow whose trust boundary and failure behavior are visible in the diff.
Need help with CI/CD automation? I help startups build AI products and automate processes - belov.works.
Frequently Asked Questions
Should you pin GitHub Actions to a full SHA or is a version tag like @v7 acceptable?
How do you handle CI/CD for a monorepo where only some services changed?
dorny/paths-filter to detect which services have changed, then gate expensive jobs (tests, builds, deploys) behind conditional steps. Each service gets its own set of filter rules; jobs only run when relevant paths are modified. This prevents a documentation change in one service from triggering a full build and deploy of unrelated services. For very large monorepos, GitHub's native path filtering in on.push.paths can reduce trigger frequency, but it doesn't give you the granularity to conditionally run specific jobs within a workflow - paths-filter does.
What's the right strategy for caching dependencies in a GitHub Actions workflow?
actions/setup-node or actions/setup-python. In a monorepo, point cache-dependency-path at the relevant lockfile; use actions/cache when you need a custom scope or restore policy. For Docker layers, cache-from: type=gha uses the GitHub Actions cache backend. Include a lockfile hash in dependency cache keys so dependency changes invalidate old entries.