LearnXops

Hey — It's Sandip Das 👋

After learning GitHub Actions basics, it’s time to level up with advanced features like advanced matrix builds, job dependencies, reusable workflows, caching, artifacts, secrets, and environments. These capabilities help you scale your automation for real-world DevOps and CI/CD pipelines.

Before we begin... a big thank you to today's sponsor, 1440 media

Fact-based news without bias awaits. Make 1440 your choice today.

Overwhelmed by biased news? Cut through the clutter and get straight facts with your daily 1440 digest. From politics to sports, join millions who start their day informed.

Sign up now!

💡Learning points:

🔹 Understanding job dependencies, needs, and parallel execution
🔹 Using matrix builds to test across multiple versions/environments
🔹 Creating reusable workflows across multiple repos
🔹 Advanced secrets and environment protections
🔹 Implementing caching for faster workflows (actions/cache)
🔹 Uploading & downloading artifacts (e.g., reports, logs, builds)
🔹 Using outputs, env, run, if, and continue-on-error smartly
🔹 Triggering workflows from other workflows (workflow chaining)
🔹 Manual approvals & policies







🧠 Learn here:

Download a high-resolution copy of this diagram here for future reference.

Understanding job dependencies, needs, and parallel execution

Jobs:

Each workflow can have multiple jobs, and by default, they run in parallel unless you specify dependencies using the needs keyword.

⚙️ needs: Define Job Dependencies

  • Use needs to define dependencies between jobs.
  • If job B needs to wait for job A, use:

jobs: job_a: runs-on: ubuntu-latest steps: - run: echo "This is Job A" job_b: needs: job_a runs-on: ubuntu-latest steps: - run: echo "This is Job B"

🧠 job_b will only run after job_a completes successfully.

⛓️ Chaining Dependencies

You can chain dependencies:

jobs: job_a: ... job_b: needs: job_a job_c: needs: job_b

Or use fan-out/fan-in style:

jobs: build: ... lint: ... test: needs: [build, lint]

🌀 test will run only after both build and lint succeed.

🚀 Parallel Execution

By default:

  • All jobs run in parallel unless needs is used.

This is parallel:

jobs: job1: ... job2: ...

This is sequential:

jobs: job1: ... job2: needs: job1


Using matrix builds to test across multiple versions/environments

Matrix builds in GitHub Actions let you run the same job across multiple versions/environments — super useful for testing against multiple OSes, languages, or app versions in parallel.

Syntax of a Matrix Build

jobs: test: runs-on: ubuntu-latest strategy: matrix: node: [14, 16, 18] name: Node.js ${{ matrix.node }} tests steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm install - run: npm test

This will run 3 parallel jobs:

  • Node 14
  • Node 16
  • Node 18

Testing Across OS & Versions:

strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python: [3.8, 3.9, 3.10]

This creates 9 combinations (3 OS × 3 Python versions).

jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] python: [3.8, 3.9] name: Python ${{ matrix.python }} on ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: pip install -r requirements.txt - run: pytest


Creating reusable workflows across multiple repos

Creating reusable workflows in GitHub Actions lets you define a workflow once and call it from multiple repositories, saving time and ensuring consistency across projects.

🧩 Step-by-Step: Reusable Workflow Setup

Step 1: Create the Reusable Workflow (Caller-Friendly)

Put this in a centralized repo, e.g., org/workflows/.github/workflows/deploy.yml:

# deploy.yml (REUSABLE WORKFLOW) name: Reusable Deploy Workflow on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_TOKEN: required: true jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Echo inputs run: | echo "Environment: ${{ inputs.environment }}" echo "Using token: ${{ secrets.DEPLOY_TOKEN }}"

Step 2: Call the Reusable Workflow from Another Repo

In another repo's workflow file:

# .github/workflows/use-deploy.yml name: Call Reusable Deploy on: push: branches: [main] jobs: call-deploy: uses: org/workflows/.github/workflows/deploy.yml@main with: environment: production secrets: DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}


🔐 Advanced secrets and environment protections

In GitHub Actions, advanced secrets and environment protections help you secure deployments and control access to sensitive operations. Here's a breakdown of the powerful tools available:

1. Secrets at Different Levels

  • Repository-level: Stored in Settings > Secrets and variables > Actions
  • Organization-level: Available to all or selected repos
  • Environment-level: Bound to specific GitHub environments (e.g., staging, production)

2. Best Practices

  • Use environment secrets for sensitive operations like production deployments.
  • Avoid echoing secrets (set +x, echo $SECRET) — use secrets.<NAME> only in secure contexts.

3. Encrypted at Rest & Transit

  • GitHub encrypts secrets automatically and never displays them once set.

Environments and Protection Rules

What Are Environments?

Used to model stages like dev, staging, prod.

Features:

  • Environment-specific secrets
  • Required reviewers
  • Deployment branches restrictions
  • Wait timers (manual delay before deployment)

Example:

jobs: deploy: runs-on: ubuntu-latest environment: name: production url: https://myapp.com steps: - run: ./deploy.sh env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

Advanced Features:

1. Manual Approvals

Enable via GitHub UI under Settings → Environments → production → Protection rules:

  • Add required reviewers before deployment starts.

2. Branch Restrictions

Limit which branches can trigger deployments to an environment.

3. Wait Timers

Delay execution by a set amount of time (useful for cooldown before production deploys).

Implementing caching for faster workflows (actions/cache)

Implementing caching using actions/cache in GitHub Actions is a powerful way to speed up workflows by reusing dependencies like node_modules, ~/.cache, or vendor folders across runs.

Here’s how to do it right 👇

Let’s understand first why we Use Caching:

  • Avoid reinstalling dependencies every time
  • Great for package managers like npm, yarn, pip, composer, etc.
  • Can significantly cut down CI times

Basic Example: Node.js + npm

jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Cache node modules uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: npm ci - name: Build run: npm run build

How it works:

  • key determines when to cache
  • restore-keys allows fallback to older caches
  • path is what gets cached

Example: Python + pip

- name: Cache pip uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}

Example: Go Modules

- name: Cache Go modules uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}


Uploading & downloading artifacts (e.g., reports, logs, builds)

Uploading and downloading artifacts in GitHub Actions is perfect for sharing build outputs, test reports, logs, or any generated files between jobs or after workflows finish.

GitHub provides the actions/upload-artifact and actions/download-artifact actions to manage this

Use actions/upload-artifact to store files during the workflow:

- name: Upload test results uses: actions/upload-artifact@v4 with: name: test-results path: ./reports/

Downloading Artifacts (in the same or separate jobs)

If you're using multi-job workflows, use needs to ensure one job can access another’s artifact.

jobs: build: runs-on: ubuntu-latest steps: - name: Build binary run: make build - name: Upload binary uses: actions/upload-artifact@v4 with: name: my-binary path: ./dist/myapp deploy: needs: build runs-on: ubuntu-latest steps: - name: Download binary uses: actions/download-artifact@v4 with: name: my-binary - name: Deploy run: ./myapp

By default, download-artifact downloads to the current working directory (you can set path: too).

Retention & Cleanup

Artifacts are stored for 90 days by default, but you can set it:

- name: Upload logs uses: actions/upload-artifact@v4 with: name: logs path: ./logs/ retention-days: 7


Using outputs, env, run, if, and continue-on-error smartly

outputs: Pass data between jobs or steps.

jobs: job1: outputs: version: ${{ steps.setver.outputs.ver }}

env :Set environment variables globally or per step.

env: NODE_ENV: production

run:Execute shell commands.

- run: echo "Deploying..."

if: Conditionally run steps or jobs.

- if: success() && github.ref == 'refs/heads/main'

continue-on-error: Don’t fail the job if step fails.

- run: ./flaky-script.sh continue-on-error: true

 
Manual approvals and policies

Manual Approvals

Enable in the UI:

  • Go to Settings → Environments → production
  • Add required reviewers

📦 Effect: Deployment pauses until an authorized user approves.

Branch Restrictions & Policies

You can set:

  • ✅ Allowed branches for deployment (e.g., only main)
  • ⏱️ Wait timers (e.g., delay before deploy starts)
  • 🛑 Prevent auto-deploys from forks

📖 Learning Resources

📌 GitHub Actions Advanced Features
📌 Matrix Strategy Docs
📌 Reusable Workflows Guide
📌 Caching Dependencies
📌 Environments & Deployment Protection Rules
📌 Workflow Inputs and Outputs




🔥 Challenges

🔹 Challenge 1: Use a matrix to test across:

  • 3 versions of Node.js or Python
  • 2 OSs (ubuntu-latest, windows-latest)

🔹 Challenge 2: Use needs: to create a pipeline like:

  • build → test → deploy (conditionally run deploy if tests pass)

🔹 Challenge 3: Use actions/cache@v3 to store build dependencies and speed up CI

🔹 Challenge 4: Add workflow_dispatch inputs like environment: [dev, prod] and print them in the job

🔹 Challenge 5: Add an if: condition to only run a deployment job on the main branch

🔹 Challenge 6: Upload a build artifact in one job and download it in another

🔹 Challenge 7: Create a reusable workflow called from another workflow using workflow_call

🔹 Challenge 8: Define a protected environment with manual approval and use it in a deploy job

🔹 Challenge 9: Use output from one job (e.g., version, build_hash) in the next job

🔹 Challenge 10: Use a GitHub Action to build a Docker image, tag it, and push to Docker Hub

🤷🏻 How to Participate?

✅ Complete the tasks and challenges.
✅ Document your progress and key takeaways on GitHub ReadMe, Medium, or Hashnode.
✅ Share the above in a LinkedIn post tagging me (Sandip Das), and use #60DaysOfDevOps to engage with the community!

Share this post