Automatic deployment on every git push is one of the most productive workflows a development team can adopt. It removes the manual deployment step, reduces human error, and keeps your production environment in sync with your main branch at all times.
But automatic deployment without automated testing first is a liability. Every push goes to production, which means every broken commit, every failing test, every unreviewed dependency vulnerability goes to production too.
This guide shows you how to set up a GitHub Actions CI pipeline that runs your test suite, linting, and security checks before each deployment. The goal is a clean separation of concerns: GitHub Actions handles quality control, and your deployment platform handles the deployment.
The CI/CD Pipeline
Continuous integration and continuous deployment are often discussed together, but they serve distinct purposes.
Continuous integration (CI) is the practice of automatically running tests and checks every time code is pushed. The goal is to catch problems as close to the source as possible — immediately after a commit, not hours later during a manual review.
Continuous deployment (CD) is the practice of automatically deploying code to production when it passes all checks. The goal is to eliminate the manual release step.
When you connect a GitHub repository to Out Plane and configure a deployment branch, Out Plane handles the CD side automatically. Every push to that branch triggers a new deployment: Queued → Building → Deploying → Ready. No manual action required.
The gap this guide fills is the CI side. Without a CI pipeline in front of that auto-deploy, your flow looks like this:
Push to main → Auto-deploy (always, regardless of test results)
With GitHub Actions configured as a quality gate, the flow becomes:
Push to main → GitHub Actions (lint, test, scan) → Pass → Auto-deploy
→ Fail → No deploy, fix required
Branch protection rules complete the picture by preventing broken code from reaching the deployment branch in the first place. When a pull request's CI checks fail, the merge is blocked.
Continuous deployment without continuous integration is just continuous deployment of bugs.
What You'll Set Up
By the end of this guide, you will have:
- Automated test execution on every push and pull request
- Linting and code quality checks
- Security scanning for known vulnerabilities
- Branch protection rules that enforce passing CI before merge
- Dependency caching for fast pipeline execution
- A production-ready workflow file you can adapt for any project
Step 1: Basic Test Workflow
Create the directory .github/workflows/ in your repository root if it does not already exist. Add a file named ci.yml.
Here is a complete workflow for a Node.js application:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm testThis workflow runs on two triggers: pushes to main and pull requests targeting main. The cache: 'npm' option tells the runner to cache the node_modules directory between runs, which cuts install time significantly on repeated runs.
npm ci is used instead of npm install because it installs exactly the versions recorded in package-lock.json. This makes builds deterministic and prevents dependency drift between your local machine and CI.
Python variant
For a Python application using pytest:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txt
- run: ruff check .
- run: pytestGo variant
For a Go application:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
cache: true
- run: go vet ./...
- run: go test ./...Step 2: Add Database Testing
Applications that interact with a database need a database available during tests. GitHub Actions supports service containers — Docker containers that run alongside your job and are accessible over localhost.
Here is the Node.js workflow extended with a PostgreSQL service:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test_db
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run db:migrate
- run: npm testThe options block configures a health check so GitHub Actions waits until PostgreSQL is ready to accept connections before running your steps. Without this, your migration step would fail with a connection error.
The DATABASE_URL environment variable at the job level makes it available to all steps. Your test suite reads this variable the same way it reads any environment variable in production.
For Redis, add a second service alongside PostgreSQL:
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5Step 3: Security Scanning
Security checks are easy to skip when you are moving fast. Automating them in CI removes the willpower requirement — they run on every push whether you remember to run them locally or not.
Dependency vulnerability scanning
For Node.js, npm audit checks your installed dependencies against a database of known vulnerabilities:
- name: Audit dependencies
run: npm audit --audit-level=highThe --audit-level=high flag fails the job only for high-severity vulnerabilities. Use --audit-level=moderate for stricter enforcement.
For Python:
- name: Install pip-audit
run: pip install pip-audit
- name: Audit dependencies
run: pip-auditCodeQL static analysis
GitHub's CodeQL action performs static analysis to find security vulnerabilities in your source code. It detects issues like SQL injection vectors, unsafe deserialization, and cross-site scripting risks.
Add a separate job to your workflow:
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3Replace javascript with python, go, java, ruby, or csharp as appropriate for your project.
Dependency review on pull requests
The dependency review action checks pull requests for newly introduced vulnerable dependencies before they are merged. It runs only on pull requests, which is the right place to catch new dependencies being added:
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4This action compares the dependency manifest in the base branch against the head branch and flags packages with known CVEs.
Step 4: Branch Protection
Workflow files alone do not prevent broken code from reaching your deployment branch. A developer can push directly to main, bypassing pull requests and CI entirely. Branch protection rules enforce the policy at the repository level.
To configure branch protection on GitHub:
- Go to your repository on GitHub
- Navigate to Settings > Branches
- Under Branch protection rules, click Add rule
- Set Branch name pattern to
main - Enable Require a pull request before merging
- Enable Require status checks to pass before merging
- In the search box that appears, add your CI job names (e.g.,
test,codeql) - Enable Require branches to be up to date before merging
- Optionally enable Require review from Code Owners
- Click Save changes
With these rules in place, no pull request can be merged to main until all required status checks report success. Direct pushes to main can also be blocked by enabling Restrict pushes that create matching branches.
The result: the only way code reaches main is through a pull request with passing CI. And since Out Plane deploys on push to main, the only code that auto-deploys is code that has passed all your checks.
Step 5: Environment-Specific Workflows
Not every check needs to run at the same speed. Slow checks during local iteration add friction. Fast feedback on pull requests builds confidence. Structuring your workflows by context lets you optimize both.
Fast checks on pull requests
Pull requests benefit from fast feedback — developers are waiting on the result. Keep the PR workflow focused on the checks that catch the most common problems:
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm test -- --coverageFull pipeline on main
The main branch workflow can afford to be more thorough since it runs after the merge decision is made:
name: CI
on:
push:
branches: [main]
jobs:
test:
# ... full test suite with database services
codeql:
# ... static analysis
dependency-review:
# ... skipped on push, only runs on PRsDependency caching
Cache your dependency installation across runs. GitHub Actions caches are keyed by a hash of the lock file, so the cache is automatically invalidated when dependencies change:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'For Python:
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'Cached installs typically complete in 5-15 seconds instead of 30-90 seconds, which adds up significantly across dozens of daily runs.
Advanced: Multi-Stage Pipeline
As your test suite grows, parallel jobs reduce total pipeline time. Jobs that do not depend on each other can run simultaneously.
Parallel jobs
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test_db
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run db:migrate
- run: npm test
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm audit --audit-level=highlint, test, and security now run in parallel. Total pipeline time equals the duration of the slowest job, not the sum of all jobs.
Matrix testing
Matrix builds test your application against multiple runtime versions simultaneously. This is useful for libraries or applications that need to support a range of Node.js or Python versions:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18', '20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm testThis creates three parallel jobs, one per Node.js version. All three must pass for the workflow to succeed.
The Complete Workflow
Here is a production-ready ci.yml that combines all the pieces above. This is a practical starting point for a Node.js application with a PostgreSQL database:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint and type check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/test_db
NODE_ENV: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run database migrations
run: npm run db:migrate
- name: Run tests
run: npm test -- --coverage
security:
name: Security scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Audit dependencies
run: npm audit --audit-level=high
dependency-review:
name: Dependency review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4
codeql:
name: CodeQL analysis
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with:
languages: javascript
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3Commit this file as .github/workflows/ci.yml and push it to your repository. GitHub will detect the file and start running the workflow immediately.
How This Works with Auto-Deploy
Once your CI pipeline is in place and branch protection rules are configured, the full flow looks like this:
A developer opens a pull request:
- GitHub Actions runs the CI workflow against the PR branch
- Lint, test, and security jobs run in parallel
- Results appear as status checks on the pull request
- If any job fails, the merge is blocked
- The developer fixes the issue, pushes again, and CI re-runs
The pull request is approved and merged to main:
- The merge push triggers the CI workflow again on
main - If CI passes (it should, since the PR checks already passed), the push stays on
main - Out Plane detects the push to the configured branch
- A new deployment begins: Queued → Building → Deploying → Ready
- Your updated application is live
A push fails CI:
- GitHub Actions reports a failure
- The push is already on
mainat this point — CI runs after the push, not before - The deployment still triggers on Out Plane
This is why branch protection on pull requests matters more than CI on direct pushes to main. The real quality gate is preventing broken code from reaching main in the first place. If your team enforces the pull request workflow, code on main has already passed CI by the time it triggers auto-deploy.
Your deployment platform handles the deployment. GitHub Actions handles the quality gate. The two systems complement each other without overlap.
Monitoring Your Pipeline
GitHub Actions dashboard
The Actions tab in your GitHub repository shows all workflow runs, their status, duration, and logs. You can drill into any run to see the output of individual steps — useful for debugging flaky tests or slow build steps.
Workflow run notifications
GitHub sends email notifications for failed workflow runs by default. You can configure notification preferences under Settings > Notifications in your GitHub account. For teams, consider routing failure notifications to a shared Slack channel using the slackapi/slack-github-action action.
Build time optimization
Pipeline time directly affects developer velocity. A 10-minute CI run creates a 10-minute feedback loop. Keep an eye on total run time and optimize aggressively:
- Dependency caching: The single highest-impact optimization. Use
cache: 'npm'orcache: 'pip'on setup actions. - Parallel jobs: Split lint, test, and security into separate parallel jobs instead of running them sequentially.
- Test splitting: For large test suites, use a test splitting tool to distribute tests across multiple parallel runners.
- Skip unchanged paths: Use
pathsfilters on workflow triggers to skip CI for documentation-only changes:
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'Target pipeline times:
- Under 3 minutes: good
- 3-7 minutes: acceptable, worth optimizing
- Over 7 minutes: investigate — developers will start skipping reviews to avoid waiting
Summary
Automatic deployment is a productivity multiplier. Automatic deployment without automated testing is a risk amplifier. GitHub Actions CI bridges the gap.
Here is what this guide covered:
- Basic workflow:
ci.ymlwith lint and test steps, running on push and pull request - Database testing: Service containers for PostgreSQL and Redis available during test runs
- Security scanning:
npm audit, CodeQL static analysis, and dependency review - Branch protection: Requiring CI status checks before pull request merges
- Environment-specific workflows: Fast PR checks, full pipeline on main, dependency caching
- Parallel jobs: Running lint, test, and security simultaneously to reduce total pipeline time
- Auto-deploy integration: How CI and Out Plane's auto-deploy work together as a complete pipeline
The workflow files in this guide are practical starting points. Adjust the test commands, Node.js version, and service configuration to match your application. The structure — lint, test, security, all in parallel, all required before merge — applies regardless of language or framework.
For next steps on the deployment side, see how to deploy a Next.js application or deploy a Docker application on Out Plane. If you are optimizing an existing deployment, the zero-downtime deployment guide covers rolling updates and health check configuration.
Ready to connect your repository and enable auto-deploy? Get started with Out Plane — GitHub OAuth, no CLI required.