GitHub Actions & CI/CD Pipeline Mastery: The Complete Guide
In modern software engineering, Continuous Integration and Continuous Deployment (CI/CD) is not a luxury—it is the foundational nervous system of any high-performing engineering team. As of 2026, GitHub Actions has established absolute dominance over legacy systems like Jenkins, TravisCI, and CircleCI by integrating automation directly into the repository layer.
However, the gap between writing a basic npm run test script and engineering a Platinum Standard, enterprise-grade deployment pipeline is massive. Poorly configured workflows suffer from 20-minute build times, brittle tests, exorbitant billing costs, and critical security vulnerabilities through leaked secrets.
In this exhaustive, 3,500+ word technical guide, we will dissect GitHub Actions from the ground up. You will learn the core architectural primitives, advanced YAML matrix strategies, aggressive Docker layer caching, OpenID Connect (OIDC) security moats, and how to architect deployment pipelines that rival elite tech companies.
1. Core Architecture: Workflows, Jobs, and Steps
Before writing any YAML, you must understand the exact hierarchy and execution boundaries within GitHub Actions. A common mistake is confusing when variables are shared and when execution is isolated.
The Workflow
A Workflow is a configurable automated process defined by a YAML file placed strictly inside the .github/workflows/ directory of your repository. A repository can contain dozens of workflows (e.g., one for Pull Request linting, one for nightly Cron jobs, and one for Production Deployments). Workflows are triggered by Events.
Jobs and Runners
Inside a workflow, work is divided into Jobs. By default, every job in a workflow executes in parallel and completely independently on a fresh, pristine virtual machine called a Runner (such as ubuntu-latest, windows-latest, or macos-latest).
Because jobs run on physically isolated VMs, they do not share disk space or memory. If Job A compiles a React app into a dist/ folder, Job B cannot simply copy it. You must explicitly declare dependencies between jobs using the needs: [job_a] keyword (which forces sequential execution) and transfer files using the Artifacts API.
Steps and Actions
Within a Job, execution is broken down into sequential Steps. Steps run on the same Runner, meaning they share the same filesystem and memory namespace. A step can execute a raw shell script (using the run keyword) or invoke an Action (using the uses keyword).
Actions are pre-packaged, reusable units of code written by GitHub or the open-source community. For example, uses: actions/checkout@v4 is a standard action that securely clones your repository code onto the runner's disk, saving you from writing complex git clone and authentication scripts manually.
Example: The Architectural Hierarchy
name: Production Build
on:
push:
branches: ["main"]
jobs:
build_app:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install & Build
run: |
npm ci
npm run build
deploy_app:
needs: [build_app]
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..." 2. Mastering Event Triggers and Concurrency
Workflows execute in response to GitHub Events. While most developers know on: push, mastering enterprise pipelines requires granular control over exactly when and how workflows trigger.
Granular Push and PR Triggers
Running extensive end-to-end tests on every single commit is an excellent way to burn through your CI budget. You should aggressively filter triggers using paths and branches.
on:
pull_request:
branches: ["main", "staging"]
paths:
- 'src/**'
- 'package.json'
paths-ignore:
- 'docs/**'
- '**.md'
In this configuration, if a developer only updates a README.md file, the workflow instantly bypasses execution, saving minutes of compute time.
Manual Dispatches and Input Parameters
The workflow_dispatch event allows you to manually trigger a workflow via the GitHub UI, the GitHub CLI, or a Webhook payload. It is invaluable for one-off operations like database migrations, cache clearing, or rollback deployments.
on:
workflow_dispatch:
inputs:
environment:
description: 'Target Environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
dry_run:
description: 'Simulate deployment only'
required: false
type: boolean Concurrency: Canceling Redundant Runs
If a developer pushes a commit, realizes a typo, and pushes a second commit 10 seconds later, GitHub Actions will traditionally spawn two parallel workflows. The first workflow is now obsolete, but it will continue consuming compute minutes and might even cause deployment race conditions.
The concurrency group entirely eliminates this problem. By grouping runs based on the Git ref (branch name), GitHub will automatically cancel any in-progress runs when a new push occurs.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true 3. Dynamic Matrices and Parallel Execution
When building open-source libraries or cross-platform desktop applications, ensuring compatibility across multiple environments is critical. Writing separate jobs for Ubuntu, Windows, and macOS across Node.js 18, 20, and 22 would require thousands of lines of duplicated YAML.
The Strategy Matrix
The strategy.matrix acts as a multiplier. By supplying arrays of variables, GitHub computes the Cartesian product and spawns a unique parallel job for every combination.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
This simple block generates 9 simultaneous jobs. The fail-fast: false directive ensures that if the Windows/Node 18 job fails, the remaining 8 jobs continue executing to completion, allowing you to isolate platform-specific bugs accurately.
Matrix Exclusions and Additions
Sometimes, certain combinations are invalid or unsupported. You can prune the matrix using exclude or inject highly specific single combinations using include.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20]
exclude:
# Windows does not support our Node 18 native bindings
- os: windows-latest
node: 18
include:
# Test a bleeding-edge nightly build explicitly on Ubuntu
- os: ubuntu-latest
node: 23-nightly 4. Caching, Artifacts, and Build Speed Optimization
Slow pipelines cripple developer velocity. If a CI run takes 25 minutes, developers lose focus and context-switch, creating massive productivity drains. The primary culprits behind slow builds are network I/O (downloading the same npm modules repeatedly) and CPU bottlenecks (recompiling unchanged native binaries or Docker layers).
NPM & Dependency Caching
Every runner starts completely blank. Running npm install downloads hundreds of megabytes of identical packages on every run. Implementing the actions/cache mechanism allows you to save the node_modules folder to GitHub's internal fast-storage and restore it in seconds.
steps:
- uses: actions/checkout@v4
- name: Cache Node Modules
uses: actions/cache@v4
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm ci
The key utilizes a hash of the package-lock.json file. If your dependencies remain unchanged, the hash matches the cache key, and the folder is instantly downloaded. If a developer adds a new package, the hash changes, resulting in a cache miss and a fresh installation, followed by a new cache upload at the end of the run.
Note: Modern setup actions like setup-node, setup-python, and setup-go now feature built-in caching simply by adding cache: 'npm' to their parameters, largely replacing the need for raw manual cache configurations.
Docker Layer Caching (DLC)
If your deployment pipeline relies on building Docker containers, compiling the image from scratch every time is devastatingly slow. Docker natively supports caching intermediate build layers, and GitHub Actions can utilize external registries or the specialized gha (GitHub Actions) cache backend.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: my-registry.com/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
The type=gha parameter instructs Buildx to export all intermediate Docker layers directly into the GitHub Actions cache API. The mode=max ensures that even intermediate layers that weren't part of the final image output are cached, resulting in hyper-optimized sub-minute builds. For deeper Docker insights, review our Complete Docker Compose Guide.
Artifacts: Cross-Job File Transfers
When Job A creates a production .zip bundle and Job B deploys it, you cannot rely on the filesystem. Artifacts are the official mechanism for archiving outputs from a runner.
# Inside Job A (Build)
- name: Upload Build Output
uses: actions/upload-artifact@v4
with:
name: production-dist
path: dist/
retention-days: 5
# Inside Job B (Deploy)
- name: Download Build Output
uses: actions/download-artifact@v4
with:
name: production-dist
path: deploy-folder/ 5. Enterprise Security: OIDC, Environments, and Secret Scanners
CI/CD systems hold the keys to the kingdom. If an attacker gains access to your GitHub Actions environment, they can steal production database passwords, AWS credentials, and deployment keys. Hardening your pipeline is non-negotiable.
The Fall of Long-Lived Credentials
Historically, deploying to AWS required creating an IAM User, generating a permanent AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, and storing them in GitHub Secrets. If these keys were ever accidentally logged or exfiltrated, attackers had permanent access until manual rotation occurred.
The Rise of OpenID Connect (OIDC)
OIDC radically shifts the security paradigm. Instead of storing permanent keys, GitHub Actions operates as an Identity Provider. AWS, Azure, and GCP are configured to trust GitHub's cryptographic signatures. When a workflow runs, GitHub generates a signed JSON Web Token (JWT) asserting the repository name and branch.
The workflow presents this JWT to AWS. AWS cryptographically verifies the token and issues a temporary, short-lived session token valid for only 60 minutes.
permissions:
id-token: write # Required to generate OIDC JWT
contents: read # Required to clone the repository
steps:
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
aws-region: us-east-1
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-production-bucket By implementing OIDC, there are zero static secrets stored in GitHub. Even if the workflow execution is fully compromised, the attacker only acquires a temporary token that expires before they can successfully exploit the broader infrastructure.
GitHub Environments and Protection Rules
For production deployments, pushing to the main branch should not trigger an automatic deploy without human oversight. GitHub Environments provide approval gates and environment-specific secrets.
jobs:
deploy_production:
runs-on: ubuntu-latest
environment:
name: production
url: https://www.mywebsite.com
steps: ... When this job initiates, it pauses execution. A notification is sent to the engineering managers. Only after a human clicks "Approve" in the GitHub UI will the job receive the production secrets and proceed with the deployment.
Preventing Secret Leakage
Even with GitHub's automatic secret masking (which replaces printed secrets with *** in logs), developers can inadvertently expose secrets by echoing them into files or base64 encoding them. Always utilize our local Secret Scanner to pre-validate source code, and strictly limit the permissions block inside workflows to adhere to the Principle of Least Privilege.
6. Advanced Deployment Patterns: SSH, SSH Tunnels, and Webhooks
While modern stacks utilize Kubernetes and declarative GitOps via ArgoCD, thousands of reliable enterprises still rely on traditional VM deployments utilizing SSH and Systemd. Automating these pipelines via GitHub Actions requires precision.
Zero-Downtime Deployments via SSH
Deploying directly to a Linux server often involves copying files, restarting a Systemd service, and reloading Nginx. Executing this securely requires utilizing GitHub Secrets for your SSH Keys.
- name: Secure Rsync and Restart
uses: appleboy/[email protected]
with:
host: ${{ secrets.PROD_HOST }}
username: deployer
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /var/www/app
git pull origin main
npm ci --production
sudo systemctl restart my-app.service
sudo nginx -s reload Crucial Security Note: Never run deployment commands as the root user. Create a dedicated deployer user, configure highly restrictive Linux ACLs, and use the visudo file to grant passwordless sudo access only for specific systemctl commands.
VPNs, Jump Hosts, and Network Isolation
Production servers should never expose Port 22 (SSH) to the public internet. If your infrastructure resides in a private AWS VPC, GitHub's public IP addresses cannot reach it directly.
To solve this, modern pipelines deploy via a Bastion server utilizing SSH ProxyJump, or they establish a temporary VPN tunnel (like Tailscale or WireGuard) directly inside the runner VM prior to executing the deployment steps.
- name: Connect to Tailscale VPN
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Deploy to Private Subnet Server
run: ssh [email protected] 'bash ./update.sh' 7. Cost Optimization and Self-Hosted Runners
GitHub provides a generous free tier of actions minutes, but enterprise repositories running heavy integration tests, Docker builds, and matrix jobs can incur thousands of dollars in monthly compute fees. The default ubuntu-latest runners utilize modest 2-core / 7GB RAM virtual machines. If your application takes 40 minutes to compile on these machines, you are severely bleeding capital.
The Self-Hosted Revolution
GitHub allows you to register your own virtual machines or bare-metal servers as Self-Hosted Runners. You pay absolute zero to GitHub for compute minutes; you only pay your cloud provider for the raw infrastructure.
By spinning up a $40/month Hetzner dedicated server featuring 16 CPU cores and 64GB of RAM, you can slash compilation times from 40 minutes down to 3 minutes, while completely neutralizing GitHub's per-minute billing fees.
Security Risks of Self-Hosted Runners
Self-hosted runners maintain persistent local state. Unlike GitHub-hosted runners, which are destroyed and recycled after every single job, a bare-metal self-hosted runner executes Job B in the exact same environment that Job A just finished in.
If a malicious pull request injects code that writes a backdoor into the /tmp/ directory, subsequent jobs from other branches might execute that backdoor. Therefore, never execute untrusted pull requests from public forks on self-hosted runners. Reserve them exclusively for private repositories, highly trusted internal teams, and controlled production branches.
8. Diagnostics, Debugging, and Pipeline Best Practices
When a pipeline fails, developers often resort to "shotgun debugging"—committing random speculative fixes featuring commit messages like "fix tests", "fix again", "maybe this works." This clutters the git history and wastes immense time.
Enable Runner Diagnostic Logging
If an action fails silently or a bash script behaves unpredictably, you can force the runner to expose exhaustive internal telemetry. Navigate to your repository settings and create a secret named ACTIONS_STEP_DEBUG with the value true. The next workflow run will dump thousands of lines of deep diagnostic traces, highlighting exactly where environment variables and memory states derailed.
Local Debugging with Act
To prevent polluting your commit history, utilize the open-source tool Act. Act runs a specialized Docker container on your local workstation that perfectly mimics the GitHub Actions runtime environment.
# Install act via Homebrew
brew install act
# Run the 'push' event workflows locally
act push
# Run a specific job
act -j build_app Testing complex workflows locally inside Docker guarantees rapid feedback loops without waiting for GitHub's queuing infrastructure.
Maintain DRY Workflows with Reusable Pipelines
If your organization contains 50 microservices, maintaining 50 separate .github/workflows/deploy.yml files violates the DRY (Don't Repeat Yourself) principle. When a security protocol changes, updating 50 repositories manually is catastrophic.
Utilize Reusable Workflows (workflow_call). Define the master deployment logic centrally in an infrastructure repository. The 50 microservices simply reference the master workflow and pass in local parameters.
# Microservice repository
jobs:
call-master-deploy:
uses: MyOrg/infrastructure/.github/workflows/[email protected]
with:
app_name: "payment-gateway"
secrets: inherit 9. Infrastructure as Code: Terraform and GitHub Actions
Deploying code is only half of the Continuous Deployment equation. Modern DevOps requires deploying the infrastructure that runs the code alongside it. Integrating Terraform (or OpenTofu) directly into GitHub Actions allows teams to manage AWS VPCs, ECS Clusters, and RDS databases via pull requests (often called GitOps).
The Terraform Plan and Apply Workflow
The golden rule of Infrastructure as Code (IaC) in CI/CD is strict separation of the plan phase and the apply phase. A Terraform Plan calculates exactly what cloud resources will be created, modified, or destroyed. This plan must be attached directly to the Pull Request for human review before any destructive changes occur on the main branch.
name: Terraform CI/CD
on:
push:
branches: ["main"]
pull_request:
jobs:
terraform_plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform plan -out=tfplan
# Upload the plan artifact for review
- uses: actions/upload-artifact@v4
with:
name: tf-plan
path: tfplan PR Commenting and Automation
Reviewing raw Terraform logs inside the GitHub Actions console is a terrible developer experience. Advanced teams utilize the actions/github-script action to parse the output of terraform plan and automatically inject it as a formatted comment directly onto the Pull Request timeline.
This ensures that when a developer reviews the code, they immediately see that the PR will "Destroy 1 RDS Database and Create 2 S3 Buckets" right next to the code diff, preventing catastrophic infrastructure deletions.
10. CI/CD Strategies for Massive Monorepos
As organizations scale, many migrate toward monorepos (storing the frontend, backend, and infrastructure in a single giant repository) using tools like Nx, Turborepo, or Bazel. Monorepos utterly break naive GitHub Actions configurations because a single push will trigger testing and building for every microservice, even if only the frontend UI was changed.
Path Filtering at Scale
The first defense mechanism is aggressive path filtering. Instead of a single monolithic ci.yml, you create dedicated workflows for each microservice inside .github/workflows/ and scope them to specific directories.
name: Backend CI
on:
pull_request:
paths:
- 'apps/backend/**'
- 'packages/shared-types/**'
jobs:
test_backend:
runs-on: ubuntu-latest
steps: ...
This ensures that if a developer alters a React component inside apps/frontend, the Backend CI workflow simply never executes, saving immense time and money.
Dynamic Matrix Generation
When dealing with 50+ microservices, writing 50 workflow files becomes an architectural nightmare. The platinum standard for monorepos involves dynamic matrix generation. In this pattern, Job A analyzes the Git commit diff to determine exactly which packages were modified (using tools like nx affected). Job A then outputs a JSON array of the affected projects.
Job B then ingests that JSON array into its strategy.matrix configuration, dynamically spawning parallel jobs only for the microservices that actually need building.
jobs:
detect_changes:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- run: echo "matrix=[\"backend-api\", \"auth-service\"]" >> $GITHUB_OUTPUT
build_affected:
needs: [detect_changes]
strategy:
matrix:
service: ${{ fromJSON(needs.detect_changes.outputs.matrix) }}
steps:
- run: npm run build --workspace=${{ matrix.service }} This represents the absolute pinnacle of GitHub Actions engineering—pipelines that are infinitely scalable, entirely dynamic, and fiercely cost-optimized.
11. The 2026 Landscape: GitHub Actions vs. Jenkins vs. GitLab CI
For enterprise architecture teams, migrating a massive CI/CD pipeline from a legacy system to GitHub Actions is a multi-million dollar decision. Understanding the exact architectural differences, security boundaries, and total cost of ownership between GitHub Actions, Jenkins, and GitLab CI is critical for DevOps leaders.
The Fall of Jenkins: The Maintenance Nightmare
Jenkins was the undisputed king of CI/CD for over a decade. However, by 2026, it is widely considered a legacy burden. Jenkins relies on a centralized master server that requires constant patching, JVM tuning, and plugin updates. If the Jenkins master crashes, the entire engineering organization is paralyzed. Furthermore, Jenkins pipelines are often defined in imperative Groovy scripts, which are incredibly difficult to lint, test, and maintain compared to declarative YAML.
GitHub Actions entirely abstracts away the control plane. GitHub manages the master orchestration, the queuing system, and the webhooks. Your team only manages the declarative YAML and, optionally, the compute nodes (Self-Hosted Runners). The reduction in infrastructure maintenance overhead is staggering, often saving companies entire dedicated DevOps headcount.
GitLab CI: The Closest Competitor
GitLab CI remains the only true competitor to GitHub Actions in 2026. Both are integrated directly into the Git repository, both use declarative YAML, and both support runners. GitLab historically held a slight edge in complex Directed Acyclic Graph (DAG) pipeline orchestration.
However, GitHub Actions has pulled ahead significantly due to the GitHub Marketplace. The open-source ecosystem of pre-built Actions (uses: ...) is vast. Whether you need to deploy to AWS ECS, upload coverage to Codecov, scan for secrets with Trivy, or post a message to Slack, there is a battle-tested, community-maintained Action ready to use in one line of code. In GitLab, teams often have to write bespoke bash scripts or build custom Docker images to achieve the exact same integrations.
12. Engineering Custom Actions: TypeScript and Docker
Eventually, your team will encounter a proprietary workflow that cannot be solved using the GitHub Marketplace. Perhaps you need to query an internal LDAP server, trigger a proprietary hardware testing suite, or parse a highly specific XML report format. When shell scripts become too brittle, it is time to author a Custom GitHub Action.
Types of Custom Actions
GitHub supports three primary methods for creating custom actions: Composite Actions, JavaScript (TypeScript) Actions, and Docker Container Actions.
Composite Actions: Bundling YAML
Composite actions are the simplest to create. If you find yourself copying and pasting the exact same five run: steps across multiple workflows (e.g., installing a specific CLI tool, configuring a credential, and running a health check), you can bundle them into a single action.yml file.
# action.yml
name: 'Setup Corporate CLI'
description: 'Downloads and configures the internal CLI'
runs:
using: "composite"
steps:
- run: curl -O https://internal.corp/cli.tar.gz
shell: bash
- run: tar -xzf cli.tar.gz -C /usr/local/bin
shell: bash
Other repositories can now invoke this logic cleanly with uses: MyOrg/setup-cli@v1, drastically reducing YAML boilerplate across the organization.
TypeScript Actions: The Enterprise Standard
When you need complex conditional logic, API calls, or heavy string parsing, bash scripts fail. TypeScript Actions are executed directly by the runner's pre-installed Node.js binary. By utilizing the @actions/core and @actions/github npm packages, you gain native, strongly-typed access to the workflow's inputs, outputs, and the authenticated GitHub REST API (Octokit).
import * as core from '@actions/core';
import * as github from '@actions/github';
async function run(): Promise<void> {
try {
const token = core.getInput('github_token');
const octokit = github.getOctokit(token);
const context = github.context;
if (context.eventName === 'pull_request') {
await octokit.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: 'Custom TS Action executed successfully! 🚀'
});
}
} catch (error) {
if (error instanceof Error) core.setFailed(error.message);
}
}
run();
TypeScript actions execute in milliseconds because they do not require spinning up new Docker containers, making them the platinum standard for custom CI/CD logic. The code must be compiled into a single file (using @vercel/ncc) before pushing to the repository.
Docker Container Actions: Maximum Isolation
If your action requires a specific Linux distribution, exotic system dependencies (like Rust or C++ compilers), or legacy binaries, you should build a Docker Container Action. The action.yml simply points to a Dockerfile. When the workflow runs, GitHub automatically builds the Docker image and executes your code inside the container, mapping the repository workspace into the container volume automatically.
While Docker actions provide absolute isolation and reproducibility, they suffer a performance penalty, as the runner must build or pull the image before execution can begin.
The Future of Automation
GitHub Actions has democratized elite, hyperscale CI/CD infrastructure, making it available to individual developers and massive enterprises alike. By mastering the core execution boundaries, dynamic matrix permutations, Docker caching mechanisms, and zero-trust OIDC security integrations detailed in this massive guide, you possess the capability to construct pipelines that execute flawlessly in seconds rather than hours.
To continue expanding your DevOps mastery, explore our architectural deep-dives on Docker Compose, Nginx Reverse Proxies, and secure SSH Systems. For real-time configuration generation, leverage the ZeroData locally-processed utilities linked throughout this ecosystem, such as our Docker Env Mapper and Cron Generator.
How ZeroData protects your privacy
- ✓ No Uploads: Processing happens entirely via client-side JavaScript.
- ✓ No Storage: We do not have a database. We physically cannot save your data.
- ✓ No Tracking: We don't log what you process or track your inputs.
- ✓ Verifiable: Check your DevTools Network tab. You will see 0 outbound requests.
Frequently Asked Questions
What is the difference between a GitHub Action, a Step, and a Job?
A workflow is the overarching automated process. Workflows contain one or more jobs, which run in parallel by default. Each job executes on its own runner (VM) and consists of a sequence of steps. A step can either run a shell command directly or execute an Action, which is a pre-packaged, reusable script (like checking out code or setting up Node.js).
How do I share data between jobs in GitHub Actions?
Because jobs run on separate virtual machines, you cannot pass variables directly in memory. You must use the `actions/upload-artifact` action in the producing job to upload files (like build binaries) to GitHub's storage, and then use `actions/download-artifact` in the consuming job. Alternatively, for simple strings, you can use job outputs.
What is OIDC and why is it better than storing AWS/GCP credentials in GitHub Secrets?
OpenID Connect (OIDC) establishes a trust relationship between GitHub and your cloud provider. Instead of pasting long-lived, permanent AWS Access Keys into GitHub Secrets (which can be leaked or stolen), OIDC allows your GitHub workflow to request a temporary, short-lived access token valid only for the duration of the run. This eliminates credential rotation and greatly enhances security.
How does the strategy matrix work?
The `strategy.matrix` keyword allows you to run the same job multiple times across different variable combinations. For example, testing across `node-version: [18, 20, 22]` and `os: [ubuntu-latest, windows-latest]`. GitHub will automatically spawn 6 parallel jobs to cover every combination.
How can I reduce my GitHub Actions billing costs?
Use heavy caching (`actions/cache`) to avoid downloading NPM packages or Docker layers repeatedly. Utilize `concurrency` groups to automatically cancel obsolete in-progress runs when a developer pushes new commits. For heavy workloads, switch to self-hosted runners on your own AWS EC2 or Hetzner instances where compute is dramatically cheaper than GitHub's per-minute VM pricing.