Manual deployments follow a fairly predictable pattern. They work fine until someone on the team pushes a rushed change on a Friday, forgets to run the tests, and the bug hits production while everyone's offline. It's not a discipline problem. It's a process problem. A manual process fails exactly when there's the most pressure for it not to.
GitHub Actions solves that by putting the pipeline inside the repository. There's no separate CI server to maintain, no external integrations to configure. Workflows live in .github/workflows/ and run in response to Git events.
What CI/CD is and what GitHub Actions handles
Continuous Integration (CI) is the practice of running tests automatically every time someone pushes code. The goal is catching problems early, when context is fresh and the fix is cheap. Continuous Delivery (CD) is taking that validated code to a staging or production environment without manual intervention.
GitHub Actions can do both. It's an event-driven automation platform: when something happens in the repository, a push, a pull request opening, a tag creation, a cron schedule, the jobs you've defined run.
In early 2026, the platform processes over 71 million jobs per day and the GitHub Marketplace hosts more than 10,000 published actions across 32 categories. Practically anything you need to do already has a published action.
The anatomy of a workflow
A workflow is a YAML file inside .github/workflows/. A repository can have as many workflows as it needs. The filename is up to you, but it needs a .yml or .yaml extension.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
The on field defines which events trigger the workflow. jobs contains one or more units of work. Each job runs on a runner, which is by default an ephemeral virtual machine that GitHub provisions, uses, and destroys. Steps within a job run in sequence on the same machine.
uses: actions/checkout@v4 is a step that clones the repository onto the runner. Without it, the machine exists but has no code. It's one of the few steps that almost always appears first.
Runners, what they are and what they cost
GitHub Actions is free for public repositories and includes 2,000 minutes per month on the free tier for private repositories. For personal projects and open source, that's more than enough.
In January 2026, GitHub cut runner prices by up to 39%. Current pricing is $0.008 per minute for standard Linux runners and $0.016 per minute for larger runners. macOS is where you need to pay attention because it costs $0.08 per minute, ten times more than Linux.
The ubuntu-latest runner currently points to Ubuntu 24.04. In 2026, GitHub added new images in public preview: Ubuntu 26.04 for x64 and arm64, and Windows 11 arm64 with Visual Studio 2026.
Custom images for GitHub-hosted runners reached general availability in April 2026. That lets you define exactly what software comes preinstalled on the runner instead of installing it on every run, which reduces execution times in pipelines with many system-level dependencies.
Secrets and environments
Credentials never go in the YAML. GitHub has a secrets system at the repository and organization level that encrypts them at rest and injects them into workflows as environment variables. You define them in Settings → Secrets and variables → Actions.
Inside the workflow you reference them like this:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: ./deploy.sh
Environments add a layer of control over secrets and deployments. You can define a production environment that requires manual approval before any job using that environment runs. That's useful for avoiding a direct push to main from automatically deploying to production without review.
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production"
Secrets defined in the production environment are only accessible to jobs that declare that environment.
OIDC for connecting to Azure without secrets
Storing an Azure secret in GitHub works, but it introduces a credential that needs rotating, can leak, and tends to have broader permissions than necessary. OpenID Connect (OIDC) solves that more cleanly.
With OIDC, the workflow requests a token signed by GitHub directly during execution. Azure verifies that token against a federated credential configured on a service principal and issues temporary access credentials for that specific run. There's nothing to store as a secret. The credentials last exactly as long as the job does.
jobs:
deploy:
permissions:
id-token: write
contents: read
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: az webapp deploy --name my-app --resource-group my-rg --src-path ./dist
The three login values are variables (not secrets) because they aren't credentials themselves, they're public identifiers for the service principal. The OIDC runtime negotiates the temporary credentials in the background.
In April 2026, OIDC for GitHub Actions added support for repository custom properties as claims in the token, a feature that reached general availability. That allows writing more granular access policies in Azure based on repository attributes, not just the name or branch.
Cache and concurrency
Installing dependencies on every run is the biggest time sink in a typical pipeline. The actions/cache action saves and restores directories between runs based on a cache key.
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
setup-node with cache: npm handles node_modules caching automatically. For other languages and tools you need to configure it explicitly with actions/cache.
Concurrency control prevents multiple runs of the same workflow from overlapping. If you push three times quickly, without concurrency control three runs will queue up. With this, the in-progress run gets cancelled when a new one arrives:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
On feature branches, cancelling the previous run makes sense. On main you might not want to cancel because each run produces a deploy artifact. You can condition this using github.ref.
Updates through July and August 2026
GitHub Agentic Workflows entered public preview earlier in 2026 for automating tasks like issue triage, CI failure analysis, and documentation updates. In July, another integration landed: you can now run the GitHub Copilot CLI directly inside a workflow using the built-in GITHUB_TOKEN, with no personal access token to create or store. The same principle that already applied to Agentic Workflows extended to Copilot CLI.
Two security changes also came into effect. GitHub Enterprise Cloud with Data Residency started enforcing minimum version requirements for self-hosted runners on July 31, 2026. Runners below the minimum required version can no longer register or execute workflow jobs. GitHub Enterprise Cloud without Data Residency follows the same enforcement starting September 25, 2026.
The other change targets supply chain security: GitHub Actions now automatically holds workflow runs identified as potentially malicious in public repositories, requiring explicit approval from a collaborator with write access before the workflow executes. That prevents compromised credentials from triggering workflows without human intervention.
None of these three changes affects a standard, well-configured pipeline. They do matter for teams running outdated self-hosted runners and for open source projects that accept external contributions without prior review.
One practical observation before wrapping up
The learning curve for GitHub Actions isn't in understanding YAML. It's in understanding the execution model: what shares state between steps and what doesn't, when it makes sense to split into multiple jobs and when it doesn't, and how the github.* context gives you information about the event that triggered the workflow.
The official documentation is solid. The problem is that it covers a lot of ground and it's not always clear where the right entry point is for someone just starting. The most useful place to begin is Microsoft Learn's quickstart that combines GitHub Actions with Azure, which connects directly to most practical cloud deployment scenarios:
👉 https://learn.microsoft.com/azure/developer/github/github-actions?wt.mc_id=studentamb_510930
The information in this article is based on official GitHub documentation and verified sources at the time of publication. GitHub may update pricing, features, and platform behavior at any time. Check the official documentation at docs.github.com before making decisions based on this content.

