GitHub Actions Tutorial: CI/CD for Web Developers (2026)
Why GitHub Actions is the Default CI/CD Choice in 2026
GitHub Actions has become the dominant CI/CD platform for web developers in 2026. The integration with GitHub repositories — no external service to configure, free minutes for public repositories, generous free tier for private repositories — combined with the vast marketplace of pre-built actions has eliminated most reasons to choose alternative CI/CD platforms for standard web application deployments. Jenkins, CircleCI, and Travis CI all remain viable for specific use cases, but GitHub Actions is the correct default starting point.
Workflow Anatomy
GitHub Actions workflows are YAML files in the .github/workflows directory. A workflow consists of triggers (on), jobs, and steps. Triggers define when the workflow runs: push to specific branches, pull request events, schedule (cron), or manual dispatch (workflow_dispatch). Jobs run in parallel by default on fresh virtual machines — they can be made sequential with the needs key. Steps within a job run sequentially, sharing the same virtual machine and file system. Steps can run shell commands (run) or use pre-built actions (uses).
A Complete CI Pipeline for a React + Django App
A production CI pipeline for a full-stack web application runs on every pull request: install dependencies, run linting, run type checking, run unit tests with coverage, build the production bundle, and report status back to the PR. The pipeline should fail fast — run the fastest checks (linting, type checking) before the slowest (tests, build). Cache dependencies between runs using actions/cache to avoid reinstalling on every run. Use matrix strategy to test against multiple Node.js or Python versions simultaneously.
Deploying to a VPS with GitHub Actions
The deployment job runs after the CI pipeline passes, only on pushes to the main branch. The job SSHs to the production server and runs the deployment script. Store SSH keys and server credentials as GitHub Secrets — never hardcode them in workflow files. Use appleboy/ssh-action for simple SSH commands, or rsync for file transfers. For Docker-based deployments, build the image, push to a container registry (GitHub Container Registry is free), then SSH to the server to pull and restart the containers. ProofMatcher uses this exact pattern — the deploy.sh script pulls the latest image and runs docker compose up.
Useful Actions for Web Developers
actions/checkout: checks out the repository. actions/setup-node: installs a specific Node.js version. actions/setup-python: installs Python. actions/cache: caches directories between runs. github/codeql-action: runs security analysis. codecov/codecov-action: uploads test coverage reports. vercel/action: deploys to Vercel. Download our GitHub Actions workflow templates for Django + React deployments at proofmatcher.com.
Secrets, Variables, and Environments
Workflows often need API keys, SSH keys, or deployment tokens. Store them as encrypted repository or organisation secrets and reference them as ${{ secrets.DEPLOY_KEY }}; GitHub masks their values in logs. Non-sensitive configuration, such as a server hostname, belongs in configuration variables instead. For deployments, define environments such as staging and production. Each environment can hold its own secrets and protection rules, such as requiring a manual approval or allowing deployments only from the main branch, which prevents an accidental push from reaching production.
Faster Pipelines with Caching and Concurrency
Installing dependencies is often the slowest step. The official setup actions include built-in caching: actions/setup-node with cache: "npm" or actions/setup-python with cache: "pip" restores downloaded packages between runs. Split independent work, such as linting, unit tests, and building, into parallel jobs, and use a matrix to test several Node.js or Python versions at once. Add a concurrency group keyed by branch with cancel-in-progress: true, so pushing a new commit cancels the outdated run for the same branch and saves minutes.
Reusable Workflows and Composite Actions
When several repositories share the same build or deployment steps, avoid copying YAML between them. A reusable workflow, triggered with workflow_call, can be called from other workflows with inputs and secrets. For smaller repeated sequences of steps, a composite action groups them into one step you can reference by path. Both keep pipelines consistent and let you fix a problem in one place.
Security Best Practices
- Limit token permissions. Set
permissions: contents: readat the top of each workflow and grant additional permissions only to the jobs that need them. - Pin third-party actions. Reference actions by a full commit SHA rather than a tag, because tags can be moved to point at different code. Tools like Dependabot can keep pinned SHAs up to date.
- Be careful with pull requests from forks. The
pull_request_targettrigger runs with access to secrets, so never check out and run untrusted code from a fork in such a workflow. - Prefer OIDC for cloud deployments. Major cloud providers support short-lived credentials issued through OpenID Connect, which removes the need to store long-lived cloud keys as secrets.
- Avoid injecting untrusted input into scripts. Values such as pull request titles can contain shell commands. Pass them through environment variables rather than inserting them directly into
runscripts.
Debugging Failed Workflows
When a run fails, open the failing step's log first; the error is usually near the end. Re-run the job with debug logging enabled to see more detail, and add temporary steps that print versions or list files to confirm assumptions about the environment. For faster iteration, the open-source tool act runs many workflows locally in Docker, though not every hosted feature is reproduced exactly. Keep workflows small and readable, and name every step clearly so failures are easy to locate.
Keeping Costs Under Control
Public repositories get free minutes on GitHub-hosted runners, while private repositories have a monthly allowance that depends on your plan. Stay within it by caching dependencies, cancelling superseded runs, using path filters so documentation-only changes skip heavy test jobs, and running expensive end-to-end tests on pull requests to the main branch rather than on every push. For heavy or specialised workloads, self-hosted runners can reduce cost, but they need to be kept updated and should never run untrusted code from public forks.
A Deployment Job Step by Step
A reliable deployment workflow usually separates building from deploying. The build job installs dependencies, runs tests, creates the production build, and uploads it as an artifact with actions/upload-artifact. The deploy job depends on the build job with needs: build, runs only on the main branch and in the protected production environment, downloads the artifact, and copies it to the server, for example over SSH with rsync. After copying, it restarts or reloads the application and calls a health-check URL, failing the job if the site does not respond correctly. Keeping the previous release on the server allows a quick rollback: switch a symlink back and reload.
This structure means a failed test can never deploy, the exact tested build is what reaches production, and every deployment is recorded in the Actions history with its commit.
Scheduled and Manual Workflows
Not every workflow runs on a push. The schedule trigger uses cron syntax in UTC to run jobs such as nightly dependency audits, database backups verification, or link checks. The workflow_dispatch trigger adds a "Run workflow" button in the GitHub interface, optionally with input fields, which is ideal for manual deployments, one-off maintenance tasks, or re-running a release.
Status Checks and Branch Protection
CI only protects your code if its results are enforced. In the repository settings, protect the main branch and mark your test and build jobs as required status checks, so pull requests cannot be merged until they pass. Combine this with required reviews, and your pipeline becomes a safety net that nobody can accidentally bypass.