Prompt of the Day: Set Up a Complete CI/CD Pipeline with GitHub Actions
Series: Prompt of the Day — Part 10 of 30
The Monday Morning Story
It was 9:03 AM. Sarah, a solo founder shipping a SaaS app, merged what she thought was a two-line config fix. No tests ran. No review happened. The deploy was instant — which would have been great, except the "two-line fix" quietly broke the authentication middleware. By 9:47 AM, every user who tried to log in got a 500 error. Her customers emailed support. She rolled it back manually at 10:15 AM. She lost 72 minutes of uptime and spent the rest of the day apologizing.
Sarah didn't have a CI/CD pipeline. She had a vibe and a prayer.
This is not a rare story. A December 2025 analysis of CI/CD failures identified the exact same failure pattern across hundreds of teams: pipelines that show SUCCESS while production is silently on fire — because tests were skipped, secrets were hardcoded, and there was no rollback plan.
And the stakes keep rising. In March 2025, a supply chain attack against the widely-used tj-actions/changed-files GitHub Action — tracked as CVE-2025-30066 — exposed AWS access keys, GitHub personal access tokens, and private RSA keys from over 23,000 repositories. The attack succeeded because teams were pinning actions to version tags instead of commit hashes. A one-line mistake in your workflow YAML was all it took.
The good news: all of this is solvable with a single well-crafted AI prompt.
The Prompt
I'm building a [Python/Node.js/other] web application and I need a complete,
production-grade CI/CD pipeline using GitHub Actions. Please scaffold the full
.github/workflows/ directory with the following requirements:
1. TRIGGERS: Run on push to `main`, on all pull requests, and support manual
`workflow_dispatch` with an optional environment input (staging/production).
2. JOBS (in dependency order):
- lint: Run ESLint / flake8 / ruff with failure blocking merge
- test: Run the full test suite with coverage reporting (pytest / jest)
- build: Build and tag a Docker image, push to GitHub Container Registry
- deploy-staging: Deploy to staging automatically on PR merge
- deploy-production: Deploy to production with a manual approval gate
3. SECURITY:
- Pin all third-party actions to their commit SHA (not version tags)
- Use OIDC for cloud authentication instead of long-lived secrets
- Set explicit, minimal GITHUB_TOKEN permissions per job
- Add a dependency vulnerability scan (using `actions/dependency-review-action`)
4. PERFORMANCE:
- Cache dependencies using `actions/cache` keyed on the lockfile hash
- Run lint and test jobs in parallel
- Set a 30-minute `timeout-minutes` on all jobs
5. OBSERVABILITY:
- Post a Slack notification on deployment success or failure
- Upload test coverage reports as workflow artifacts
- Add a workflow status badge snippet for the README
Use `ubuntu-22.04` as the pinned runner (not `ubuntu-latest`).
Add inline comments explaining the purpose of each non-obvious configuration choice.
Why It Works
This prompt is effective for three reasons that most vibe coders miss.
It specifies a dependency order, not just a job list. Saying "run lint, test, build, deploy" is ambiguous — AI will sometimes generate all jobs in parallel. By saying "in dependency order," you get needs: keywords wired correctly, so a failed test actually blocks a deploy.
It separates security requirements into their own section. When security is buried in a bullet list alongside features, AI deprioritizes it. A dedicated SECURITY: section signals that these constraints are non-negotiable, not nice-to-haves. This is why the output will correctly pin actions to commit SHAs — exactly the protection that would have prevented the tj-actions CVE-2025-30066 attack.
It asks for inline comments. Generated YAML without comments is a trap. Six months later, no one remembers why concurrency is set the way it is. Asking for comments gives you runnable documentation, not just runnable code.
Here's a sample of what the generated output looks like for the core test job:
jobs:
test:
name: Run Test Suite
runs-on: ubuntu-22.04
# Pinned runner version prevents surprise breakage from ubuntu-latest changes
timeout-minutes: 30
permissions:
contents: read # Minimum viable permissions: read-only checkout
checks: write # Required for posting test result annotations
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
# ^ Pinned to commit SHA, not a tag, to prevent supply chain attacks
- name: Cache Python dependencies
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
# Cache key invalidates only when requirements.txt changes
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests with coverage
run: pytest --cov=. --cov-report=xml
- name: Upload coverage report
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
with:
name: coverage-report
path: coverage.xml
The commit SHA pinning, minimal permissions, cache key logic, and artifact upload are all present — because the prompt made each one an explicit requirement.
The Anti-Prompt
Here's what most people actually type:
# DON'T DO THIS
Create a GitHub Actions CI/CD pipeline for my app.
Why it fails: This generates a three-job workflow that runs tests and deploys on every push to every branch. There are no approval gates, secrets are referenced by name without guidance on how to store them, actions are pinned to @v3 version tags (vulnerable to supply chain attacks), the runner is ubuntu-latest (a moving target), and there's no caching — so your 4-minute pipeline becomes a 12-minute pipeline by month two as your dependency tree grows.
Worse, when you ask AI to improve it later, it patches one thing and breaks another. Specificity upfront is cheaper than iterative debugging later.
Variations
For a monorepo with multiple services:
...same prompt above, plus:
This is a monorepo with services in /api, /worker, and /frontend.
Use path filters on each job so that only changed services trigger their
corresponding build and deploy jobs. Use a matrix strategy for the test job
to run each service's test suite in parallel.
For AWS-specific deployments:
...same prompt above, but replace the deploy jobs with:
For deployment: Use OIDC with an IAM role (no long-lived AWS_SECRET_ACCESS_KEY).
Deploy to ECS Fargate using the `aws-actions/amazon-ecs-deploy-task-definition`
action. Update the task definition with the new image SHA and wait for the
service to stabilize before marking the job successful.
For a Node.js project with Vercel:
...same prompt, adapted:
Stack: Node.js 20, pnpm, Vitest for tests.
Deploy to Vercel: staging deploys via Vercel Preview URLs on every PR,
production deploys only on merge to main. Use the Vercel CLI
(`vercel --prod`) in the deploy job with VERCEL_TOKEN stored as a repo secret.
Action Items
- Check your existing workflows: are any actions pinned to
@v2or@v3instead of a commit SHA? Update them now — this is the CVE-2025-30066 attack vector. - Run the prompt above with your stack specified in the brackets. Review the generated YAML before committing.
- Confirm that your test job has
needs: [lint](or vice versa, per your preference) so failures actually block deploys. - Add
timeout-minutes: 30to every job. Default is 6 hours — a hung job will burn your Actions minutes quietly. - Set up a staging environment in GitHub (Settings → Environments) and add a required reviewer before enabling the production deploy job.
- Drop the README badge snippet from the AI output into your
README.md. Green badge = instant confidence signal for contributors.
Ask The Guild
This week's community question:
What's the most painful CI/CD failure you've shipped through? Silently broken tests? A hardcoded secret that ended up in logs? A deploy that partially succeeded and left production in a zombie state?
Share your war story in the Guild Discord — and what you added to your pipeline afterward. The best horror stories become next week's prompt variations.
Tom Hundley is a software architect with 25 years of experience. He has watched more deployments fail than he cares to count, and he believes the best pipeline is one that makes the wrong thing hard to do.