# How to Deploy with GitHub Actions
GitHub Actions has become the default CI/CD tool for projects that live on GitHub, and for good reason: it's right next to your code, the marketplace is huge, and the free tier is generous for public repos. This guide builds a real deployment pipeline and covers the security details people skip.
The anatomy of a workflow
A GitHub Actions workflow is a YAML file in .github/workflows/. It's made of events (what triggers it), jobs (what runs), and steps (the commands).
name: CI/CD
on:
push:
branches: [main]
pull_request:
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 testThis runs your tests on every push to main and on every PR. CI is the foundation — never deploy code that hasn't passed tests.
Step 1: Build and push a container image
If you deploy containers, the build typically happens in CI. Here's a job that builds and pushes to a registry, gated on tests passing.
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxTwo details that matter: tagging with ${{ github.sha }} gives every build a traceable, immutable tag, and the GitHub Actions cache (type=gha) dramatically speeds up repeat builds.
Step 2: Manage secrets the modern way (OIDC)
The old pattern was storing long-lived cloud credentials as GitHub secrets. The modern, far safer pattern is OpenID Connect (OIDC): GitHub Actions exchanges a short-lived signed token for temporary cloud credentials, so no static keys live in your repo.
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/gh-deploy
aws-region: us-east-1If you must use static secrets, store them in GitHub Encrypted Secrets, scope them per environment, and rotate them regularly. Never echo a secret to logs.
Step 3: Protect production with environments
GitHub [Environments](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment) let you add protection rules to deploys — required reviewers, wait timers, and branch restrictions.
deploy:
needs: build-and-push
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
steps:
- run: ./scripts/deploy.sh ${{ github.sha }}With a required-reviewer rule on the production environment, the deploy job pauses until a human approves — a simple, powerful guardrail against accidental prod pushes.
Step 4: Trigger the deploy
How the deploy step actually ships your code depends on your platform. Common approaches:
- Call a platform CLI or API from the workflow.
- Trigger a deploy hook (a URL the platform exposes).
- Let the platform watch your repo and deploy on push (no Actions deploy step needed at all).
That last option is worth dwelling on: if your platform already does Git-driven deploys, you might use GitHub Actions purely for *testing and validation*, and let the platform handle the actual deploy. That keeps your pipeline simpler and avoids duplicating deploy logic.
PandaStack and GitHub Actions
PandaStack deploys directly from your connected Git repo — push code, and it builds and goes live. That means a clean division of labor: GitHub Actions runs your test suite, linting, and any validation you want as a quality gate, and PandaStack handles the build-and-deploy pipeline (rootless BuildKit → Artifact Registry → Helm) when changes land on your deploy branch.
This is often the simplest, most robust setup for small teams: you get CI feedback on every PR from Actions, and you don't maintain a separate deploy script, registry login, or Helm invocation in your workflow — the platform owns that. You still get rollbacks, deploy history, and live build logs on the PandaStack side. If you prefer to gate deploys behind a passing CI run, branch protection plus PandaStack deploying only from main gives you exactly that.
A complete pipeline shape
PR opened ──► test job (lint, unit, integration)
│ passes
Merge to main ──► test ──► build image ──► [approval] ──► deployCommon pitfalls
- ❌ Deploying without a passing test gate
- ❌ Long-lived cloud credentials in secrets (use OIDC)
- ❌ Using
latesttags instead of immutable SHA tags - ❌ No environment protection on production
- ❌ Duplicating deploy logic the platform already provides
References
- [GitHub Actions documentation](https://docs.github.com/en/actions)
- [Security hardening with OpenID Connect](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
- [docker/build-push-action](https://github.com/docker/build-push-action)
- [GitHub: Using environments for deployment](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment)
Let GitHub Actions test, and let PandaStack deploy from your repo automatically. Start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).