# GitHub Actions Tutorial: Step-by-Step CI/CD Workflows

Master GitHub Actions with a step-by-step tutorial. Learn to build robust CI/CD workflows, manage secrets securely, and automate deployments effectively for production systems.

Sercan Öztürk

13 min read
0

/

# GitHub Actions Tutorial: Step-by-Step CI/CD Workflows

Most teams manually orchestrate release processes or rely on fragile, monolithic CI systems. But this approach commonly leads to inconsistent deployments, increased lead times, and significant toil for engineers at scale, directly impacting system reliability and developer velocity.


TL;DR

  • GitHub Actions provides event-driven automation directly within your repository for CI/CD.
  • Architect workflows using jobs, steps, and runners, triggered by various GitHub events.
  • Secure sensitive data using GitHub Secrets, injecting them safely into workflow environments.
  • Implement a full CI/CD pipeline, from build to deployment, with production readiness in mind.
  • Monitor workflow execution, manage costs, and plan for failure modes to maintain system reliability.


The Problem


In a fast-paced fintech environment, reliable and rapid delivery of software is not merely an advantage; it is a fundamental requirement. We constantly face pressure to reduce deployment times from weeks to hours, while simultaneously maintaining stringent security and auditability standards. Historically, many organizations have struggled with fragmented CI/CD tooling, where builds happen on one system, tests on another, and deployments through a third. This disjointed landscape creates significant operational overhead, introducing manual handoffs and increasing the mean time to recovery (MTTR) when issues arise.


Teams commonly report 30-50% increased operational overhead due to managing disparate CI/CD systems, alongside a 20-30% higher defect escape rate in production stemming from inconsistent build and deployment practices. Such inconsistencies can lead to critical outages, reputational damage, and regulatory non-compliance in a regulated industry. Automating these processes securely and consistently is paramount. GitHub Actions offers a unified platform to address these challenges, bringing CI/CD directly into your codebase and accelerating the path from commit to production with an auditable trail.


How It Works


Understanding GitHub Actions Workflows


At its core, a GitHub Action is an event-driven automation engine. You define workflows in YAML files (`.github/workflows/*.yml`) within your repository. These workflows are triggered by specific events, such as a `push` to a branch, a `pull_request` being opened, or a `schedule` for nightly builds. Each workflow consists of one or more `jobs`, which are independent units of work that can run in parallel or sequentially. Each `job` executes a series of `steps`, which can be shell commands or pre-built actions from the GitHub Marketplace. Runners, which are virtual machines hosted by GitHub or self-hosted by you, execute these jobs. This tight integration with your source control means your CI/CD pipeline lives alongside your code, evolving with it.


Leveraging Secrets for Secure Automation


Security in CI/CD pipelines is non-negotiable, especially when dealing with production systems. Workflows often require access to sensitive information like API keys, cloud credentials, or private repository tokens. GitHub Actions provides `secrets` to manage this data securely. Secrets are encrypted environment variables that are only exposed to private repositories and never written to logs or accessible via the GitHub UI after creation. You define secrets at the repository, organization, or environment level. During a workflow run, these secrets are injected into the environment of the runner, allowing your steps to access them without hardcoding sensitive data. This mechanism ensures that credentials remain protected, adhering to the principle of least privilege.


.github/workflows/deploy.yml

name: Deploy Application


on:

push:

branches:

- main


jobs:

deploy:

runs-on: ubuntu-latest

steps:

- name: Checkout code

uses: actions/checkout@v4 # Use actions/checkout@v4 for reliable checkout


- name: Deploy to Staging

run: |

$ echo "Deploying using ${{ secrets.AWSACCESSKEYID }} and ${{ secrets.AWSSECRETACCESSKEY }}"

# In a real scenario, this would involve actual deployment commands

# For example, using AWS CLI:

# $ aws s3 sync ./build s3://my-staging-bucket --delete \

# --region us-east-1 \

# --profile my-deployment-profile

env:

AWSACCESSKEYID: ${{ secrets.AWSACCESSKEYID }} # Safely inject AWS Access Key ID

AWSSECRETACCESSKEY: ${{ secrets.AWSSECRETACCESSKEY }} # Safely inject AWS Secret Access Key

# Consider using OIDC for cloud credentials where possible for enhanced security.

This workflow demonstrates how to reference GitHub Secrets securely within a job's steps.


The interaction between secrets and jobs is critical. Secrets are made available as environment variables only to the steps that explicitly reference them or to the entire job if defined at the job level. It is important to scope secrets appropriately. For cloud deployments, consider using OpenID Connect (OIDC) with cloud providers (e.g., AWS IAM Roles for Service Accounts, Azure Workload Identity) instead of long-lived access keys for enhanced security posture. This allows your GitHub Actions workflows to directly assume roles in your cloud environment, exchanging short-lived credentials, which significantly reduces the risk associated with static secrets.


Orchestrating Jobs and Steps for CI/CD with GitHub Actions


A robust CI/CD pipeline requires a clear sequence of operations: build, test, and deploy. GitHub Actions allows you to define dependencies between jobs using the `needs` keyword, ensuring stages execute in the correct order. Each job runs in a fresh environment, guaranteeing isolation. Data can be passed between jobs using `actions/upload-artifact` and `actions/download-artifact`, allowing a build job to produce an artifact (e.g., a compiled binary or Docker image) that a subsequent deploy job consumes. This artifact passing mechanism is fundamental for maintaining consistency across your pipeline stages.


.github/workflows/full-cicd.yml

name: Full CI/CD Pipeline


on:

push:

branches:

- main


jobs:

build:

runs-on: ubuntu-latest

steps:

- name: Checkout repository

uses: actions/checkout@v4


- name: Setup Node.js 20.x

uses: actions/setup-node@v4

with:

node-version: '20.x'


- name: Install dependencies and build

run: |

$ npm ci

$ npm run build # Example: builds a static site or application bundle


- name: Upload build artifact

uses: actions/upload-artifact@v4 # Upload the build output

with:

name: my-app-build-2026

path: ./build/ # Assuming build output is in a 'build' directory


test:

needs: build # This job depends on the 'build' job completing successfully

runs-on: ubuntu-latest

steps:

- name: Checkout repository

uses: actions/checkout@v4


- name: Setup Node.js 20.x

uses: actions/setup-node@v4

with:

node-version: '20.x'


- name: Install dependencies

run: $ npm ci


- name: Run unit and integration tests

run: $ npm test # Execute all tests


deploy:

needs: test # This job depends on the 'test' job completing successfully

runs-on: ubuntu-latest

environment: Production # Specify an environment for approvals and protection rules

steps:

- name: Download build artifact

uses: actions/download-artifact@v4 # Download artifact produced by the build job

with:

name: my-app-build-2026


- name: Configure AWS Credentials with OIDC

uses: aws-actions/configure-aws-credentials@v4 # Use OIDC for cloud authentication

with:

role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeploymentRole-2026

aws-region: us-east-1


- name: Deploy application to S3

run: |

$ aws s3 sync ./my-app-build-2026 s3://my-production-bucket-2026 --delete --region us-east-1

# The downloaded artifact will be in a directory named 'my-app-build-2026' in the runner's workspace

This example workflow illustrates a complete build, test, and deploy pipeline with artifact passing and OIDC.


The `environment` keyword in the `deploy` job is crucial for production deployments. It allows you to enforce protection rules, such as manual approvals, required reviewers, or wait timers, ensuring that sensitive deployments are not accidentally triggered. These rules are configured in your GitHub repository settings and provide an essential guardrail for production systems.


Step-by-Step Implementation: Building a Basic CI Pipeline


Let's walk through creating a simple GitHub Actions workflow to lint and test a Node.js application.


Step 1: Create Your Repository and Application

First, ensure you have a GitHub repository. For this example, we'll assume a basic Node.js project.


$ mkdir github-actions-example-2026

$ cd github-actions-example-2026

$ npm init -y

$ npm install express jest eslint --save-dev

$ touch index.js .eslintrc.json jest.config.js

$ git init

$ git add .

$ git commit -m "Initial project setup"

$ git branch -M main

$ git remote add origin https://github.com/YOUR_USERNAME/github-actions-example-2026.git

$ git push -u origin main

This sequence initializes a new Node.js project and pushes it to GitHub.


Expected Output: Your GitHub repository will contain the initial project files.


Step 2: Define Your Application Logic and Tests

Add some basic content to `index.js`, `.eslintrc.json`, and `jest.config.js`.


// index.js

const express = require('express');

const app = express();

const port = 3000;


app.get('/', (req, res) => {

res.send('Hello BackendStack! This is 2026.');

});


app.listen(port, () => {

console.log(`App listening at http://localhost:${port}`);

});


function add(a, b) {

return a + b;

}


module.exports = { app, add };


// .eslintrc.json

{

"env": {

"node": true,

"commonjs": true,

"es2021": true,

"jest": true

},

"extends": "eslint:recommended",

"parserOptions": {

"ecmaVersion": 12

},

"rules": {

}

}


// jest.config.js

module.exports = {

testEnvironment: 'node',

coveragePathIgnorePatterns: ["/node_modules/"],

};


Create a test file `index.test.js`:

// index.test.js

const { add } = require('./index');


describe('add function', () => {

test('should add two numbers correctly', () => {

expect(add(1, 2)).toBe(3);

});


test('should handle zero correctly', () => {

expect(add(0, 5)).toBe(5);

});

});


Update `package.json` with scripts for linting and testing:

{

"name": "github-actions-example-2026",

"version": "1.0.0",

"description": "",

"main": "index.js",

"scripts": {

"start": "node index.js",

"test": "jest",

"lint": "eslint ."

},

"keywords": [],

"author": "",

"license": "ISC",

"devDependencies": {

"eslint": "^8.57.0",

"express": "^4.19.2",

"jest": "^29.7.0"

}

}

These files provide a simple Node.js application, ESLint configuration, and Jest tests.


Commit and push these changes:

$ git add .

$ git commit -m "Add basic app logic, lint, and test scripts"

$ git push origin main


Expected Output: The project files are updated in your repository.


Step 3: Create the GitHub Actions Workflow File

Inside your repository, create a directory `.github/workflows/` and then a file named `ci.yml` within it.


.github/workflows/ci.yml

name: Node.js CI - Sercan's Example


on:

push:

branches: [ "main" ]

pull_request:

branches: [ "main" ]


jobs:

build-and-test:

runs-on: ubuntu-latest # Specify the runner environment

steps:

- name: Checkout repository code

uses: actions/checkout@v4 # Action to clone the repository


- name: Set up Node.js 20.x

uses: actions/setup-node@v4 # Action to set up Node.js environment

with:

node-version: '20.x'

cache: 'npm' # Cache npm dependencies for faster builds


- name: Install dependencies

run: $ npm ci # Install project dependencies


- name: Run ESLint

run: $ npm run lint # Execute linting checks


- name: Run Jest tests

run: $ npm run test # Execute unit tests

This YAML defines a workflow that triggers on push and pull requests, setting up Node.js, installing dependencies, linting, and running tests.


Step 4: Commit and Push the Workflow File

Add the new workflow file, commit, and push. This will automatically trigger your first GitHub Actions run.


$ git add .github/workflows/ci.yml

$ git commit -m "Add Node.js CI workflow for linting and testing"

$ git push origin main


Expected Output:

Navigate to the "Actions" tab in your GitHub repository. You will see a new workflow run initiated by your push. Click on it to see the status of each job and step. All steps (Checkout, Setup Node.js, Install dependencies, Run ESLint, Run Jest tests) should complete successfully.


Example output snippet from 'Run Jest tests' step log

$ npm run test


github-actions-example-2026@1.0.0 test jest


PASS ./index.test.js

add function

✓ should add two numbers correctly (2 ms)

✓ should handle zero correctly (0 ms)


Test Suites: 1 passed, 1 total

Tests: 2 passed, 2 total

Snapshots: 0 total

Time: 0.758 s

Ran all test suites.

A successful Jest test run output within the GitHub Actions logs.


Common mistake: Forgetting to commit the `.github/workflows/` directory. GitHub Actions only runs workflows that are present in the exact commit being evaluated. Ensure your workflow file is committed to the correct branch. Another frequent issue is incorrect indentation in YAML files; YAML is sensitive to spaces. Use a linter or a YAML validator to catch these errors early.


Production Readiness


Deploying GitHub Actions for production CI/CD requires more than just functional workflows. Robustness, security, cost-efficiency, and observability are critical.


  • Security:

Least Privilege `GITHUB_TOKEN`:* The `GITHUB_TOKEN` automatically created for each workflow has a default set of permissions. Adjust these permissions within your workflow YAML to follow the principle of least privilege, granting only what is necessary for the workflow.

Third-Party Actions:* When using actions from the GitHub Marketplace, always pin them to a full-length commit SHA (e.g., `actions/checkout@b4ffde65f46336ab88eb5afd8a7dc3b52d3a951c`) rather than a major version (`@v4`). This prevents unexpected changes or malicious updates from impacting your pipeline.

OIDC for Cloud Credentials:* As mentioned, use OpenID Connect (OIDC) to obtain short-lived credentials for cloud providers instead of storing long-lived access keys as GitHub Secrets. This significantly reduces the attack surface. For instance, AWS recommends using OIDC with GitHub Actions via `aws-actions/configure-aws-credentials` (link in citations).

Environment Protection Rules:* Utilize environment protection rules for production deployments. Require manual approvals, specific reviewers, or a wait timer before deployment, adding critical human gates.


  • Monitoring and Alerting:

Workflow Status:* Monitor the status of your workflows. GitHub provides a UI to view runs, but for critical pipelines, integrate with your existing observability stack. Use webhooks to send workflow completion/failure events to tools like Slack, PagerDuty, or custom dashboards.

Action Logs:* Workflow run logs are invaluable for debugging failures. Ensure your steps provide sufficiently verbose output without exposing sensitive information.

Custom Metrics:* For more advanced insights, consider adding steps within your workflows to push custom metrics (e.g., build duration, test coverage changes) to Prometheus or other monitoring systems.


  • Cost Management:

GitHub-Hosted Runners:* GitHub provides a generous free tier, but beyond that, usage is billed per minute. Monitor your usage, especially for frequently triggered workflows or long-running jobs.

Self-Hosted Runners:* For high-volume, resource-intensive, or security-sensitive workloads, self-hosted runners can be more cost-effective and provide more control. You manage the underlying infrastructure, but you gain full control over the environment and reduce minute-based billing.

Caching:* Leverage `actions/cache` for dependencies (e.g., `npm` modules, `maven` artifacts) to reduce build times and resource consumption.


  • Failure Modes and Edge Cases:

Idempotency:* Design deployment steps to be idempotent. Running a deployment multiple times should produce the same result without unintended side effects.

Rollback Strategy:* A CI/CD pipeline is incomplete without a clear rollback strategy. Ensure your deployment jobs can trigger a previous, known-good version of your application.

Concurrency:* Use `concurrency` groups in your workflows to prevent multiple simultaneous deployments to the same environment, which can lead to race conditions or unexpected state.

Timeouts:* Apply `timeout-minutes` at the job or workflow level to prevent runaway jobs from consuming resources indefinitely.


Summary & Key Takeaways


  • Automate Everything Early: Integrate GitHub Actions from the start to automate build, test, and deploy processes, reducing manual errors and increasing deployment frequency.

  • Prioritize Security with Secrets and OIDC: Always use GitHub Secrets for sensitive data and transition to OIDC for cloud authentication to manage credentials securely and reduce attack surfaces.

  • Architect for Reliability: Structure workflows with clear job dependencies, leverage artifacts for consistency, and implement environment protection rules for critical deployments.

  • Pin Actions to Full SHAs: Avoid floating versions of third-party actions by pinning them to specific commit SHAs to prevent unexpected and potentially malicious changes.

  • Observe and Optimize: Implement comprehensive monitoring for workflow status and logs, and manage costs proactively through caching and considering self-hosted runners for specific workloads.

WRITTEN BY

Sercan Öztürk

Staff SRE in fintech, 10 years in operations and reliability. Electrical and Electronics Engineering graduate, Koç University. Lead writer for CI/CD, SRE and GitOps content.Read more

Responses (0)

    Hottest authors

    View all

    Ahmet Çelik

    Lead Writer · ex-AWS Solutions Architect, 8 yrs · AWS, Terraform, K8s

    Alp Karahan

    Contributor · MongoDB certified, NoSQL specialist · MongoDB, DynamoDB

    Ayşe Tunç

    Lead Writer · Engineering Manager, ex-Meta, Google · System Design, Interviews

    Berk Avcı

    Lead Writer · Principal Backend Eng., API design · REST, GraphQL, gRPC

    Burak Arslan

    Managing Editor · Content strategy, developer marketing

    Cansu Yılmaz

    Lead Writer · Database Architect, 9 yrs Postgres · PostgreSQL, Indexing, Perf

    Popular posts

    View all
    Ahmet Çelik
    ·

    S3 Intelligent-Tiering vs Glacier: A Cost Analysis

    S3 Intelligent-Tiering vs Glacier: A Cost Analysis
    Ozan Kılıç
    ·

    SAST vs DAST vs IAST: Explaining AppSec Testing Tools

    SAST vs DAST vs IAST: Explaining AppSec Testing Tools
    Deniz Şahin
    ·

    BigQuery Tutorial: Quickstart for Backend Engineers

    BigQuery Tutorial: Quickstart for Backend Engineers
    Deniz Şahin
    ·

    GCP vs AWS vs Azure: Serverless Comparison 2026

    GCP vs AWS vs Azure: Serverless Comparison 2026
    Deniz Şahin
    ·

    Google Cloud Run Tutorial: Deploying Production Services

    Google Cloud Run Tutorial: Deploying Production Services
    Ahmet Çelik
    ·

    Kubernetes Tutorial: Step-by-Step Production Deployment

    Kubernetes Tutorial: Step-by-Step Production Deployment