Back to Blog
DevOps10 min read2026-07-08

How to Deploy with GitHub Actions

Build a real CI/CD pipeline with GitHub Actions — running tests, building images, managing secrets with OIDC, environment protection rules, and triggering deploys safely.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# 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 test

This 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=max

Two 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-1

If 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] ──► deploy

Common pitfalls

  • ❌ Deploying without a passing test gate
  • ❌ Long-lived cloud credentials in secrets (use OIDC)
  • ❌ Using latest tags 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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in DevOps

Browse all DevOps articles →

See also