GitHub Actions Strategy Guide

Choosing the Right Approach for Your ProjectGitHub Actions provides us with different strategies to approach CI/CD. All we need to do is choose the right one for our case.What is a Strategy in GitHub Actions?A strategy in GitHub Actions is a job-level …


This content originally appeared on Level Up Coding - Medium and was authored by Heba Aly

Choosing the Right Approach for Your Project

GitHub Actions provides us with different strategies to approach CI/CD. All we need to do is choose the right one for our case.

What is a Strategy in GitHub Actions?

A strategy in GitHub Actions is a job-level configuration that controls how a job executes, including whether it runs multiple times and how failures are handled.

Types of Strategies:-

Simple Linear workflow strategy

It’s a single job workflow that can have multiple steps, and all of them run sequentially.

name: Simple CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm install
- run: npm test
- run: npm run build

Here, as you can see, there’s only one job test that has numerous steps. These steps will be run sequentially.

If one step fails, the entire workflow fails, and this is reflected in GitHub Actions when it runs.

We can use this strategy if it’s a small, non-complex project, or if it’s a prototype to prove a concept, or if the team is new to CI/CD and wants to start with something simple.

It’s easy to understand and fast to set up, but scaling can be challenging. If one step fails, the entire workflow fails, and, of course, there is no parallelization because steps run sequentially.

Matrix Strategy

In the matrix strategy, we can run the same job across multiple configurations simultaneously.

name: Matrix Testing
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16, 18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test

Here, we run the job test multiple times with different combinations of OS and Node.js versions, which means the job will run nine times, and each time it will have one OS and one Node.js version (e.g., ubuntu-latest with Node version 16, then ubuntu-latest with Node version 18, and so on, until macos-latest with Node version 20).

The job executions, in a matrix strategy, run in parallel, which fixes the issue of a simple linear workflow.

We can use this strategy when we have cross-platform or language-specific requirements, or when we need to test across multiple environments.

Matrix strategy also helps us detect issues with specific configuration combinations early.

Of course, with a matrix strategy, we consume more resources, and debugging is more complex.

Multi-Job Pipeline Strategy

Instead of having one job that includes all the steps, we break it down into distinct jobs that may depend on each other and run in sequence or parallel.

name: Multi-Job Pipeline
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run lint

test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm test

build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run build

deploy:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: npm run deploy

Here, we have four jobs. lint , test , build , and deploy

Job test will run after the job lint is completed successfully, which is decided by the needs statement needs: lint.

Job build will run after jobs lint , and test are completed, which is decided by the needs statement needs: [lint, test] .

Job deploy will run after jobs build , which is decided by the needs statement needs: build . This step will be executed only if we push to the main branch, which is decided by the if statement if: github.ref == 'refs/heads/main .

Without needs In each job, the jobs will run in parallel.

We can use this strategy when we need separation of concerns, different jobs require different environments, or we want to stop the workflow on the first error. Additionally, it’s useful for complex applications with multiple stages.

This approach provides clear pipeline visualization and efficient resource usage, allowing us to apply parallel execution when possible.

The only thing that can get complex is job dependencies, which can be bottlenecks, and passing artifacts and variables between resources needs to be set up.

Monorepo Strategy

A monorepo is a repository that contains multiple apps or packages within a single codebase.

A monorepo strategy involves configuring workflows to run specific tasks based on which app or package has been changed, so we only run them on what’s affected.

name: Monorepo CI
on: [push, pull_request]
jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
backend: ${{ steps.changes.outputs.backend }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'packages/frontend/**'
backend:
- 'packages/backend/**'

frontend:
needs: changes
if: ${{ needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run test:frontend

backend:
needs: changes
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npm run test:backend

Here, we can see that the workflow is used to automate specific tasks in a monorepo that contains frontend and backend apps; the workflow consists of three jobs. changes , frontend , and backend .

changes Checks whether changes occur in the frontend, backend app, or both, and returns the values as output.

backend and frontend have a dependency on changes by checking the output values of changes . That was done on these two statements. if: ${{ needs.changes.outputs.frontend == 'true' }} , if: ${{ needs.changes.outputs.backend == 'true' }} .

We can use this strategy when we have multiple services, micro-frontend or micro-architecture, when teams work on different parts independently, or with a large codebase where builds are expensive.

It’s a good strategy for resource usage and scales well with repository size.

However, it can be challenging when it comes to setup and maintenance, as the path filter can become complex, and it may miss integration tests.

Environment-Based Strategy

It involves different workflows or job configurations for various deployment environments.

name: Environment Deploy
on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- run: npm run deploy:staging

deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: npm run deploy:production

Here, we have two jobs deploy-staging , and deploy-production , and based on which branch is updated, the workflow is deployed to a specific environment.

This strategy can be beneficial when we have multiple deployment environments, each with its own unique configurations and approval levels.

Additionally, this strategy allows for environmental isolation and a clear deployment history.

However, we can face issues with configuration duplication and complex branch management.

Reusable Workflow Strategy

So we create centralized, reusable workflows that can be called from multiple repositories.

# .github/workflows/reusable-ci.yml
name: Reusable CI
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '18'
secrets:
NPM_TOKEN:
required: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm install
- run: npm test
# Consumer workflow
name: Use Reusable
on: [push]
jobs:
call-reusable:
uses: ./.github/workflows/reusable-ci.yml
with:
node-version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Here, we have two workflows, one is responsible for running only tests, and the other is used to run them.

It can be helpful when we want to standardize our workflows, and if we have multiple repositories with the same CI/CD steps.

It reduces duplication and maintains consistency, making maintenance and improvement easier with this approach.

Challenges can be complex versioning if needed, the first time creation can be complex, and less flexible with unique requirements.

Feature Flag Strategy

It’s when we use conditions and feature flags to control the workflow behavior.

name: Feature Flag CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run standard tests
run: npm test

- name: Run experimental tests
if: contains(github.event.head_commit.message, '[experimental]')
run: npm run test:experimental

- name: Performance tests
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: npm run test:performance

We have observed similar conditions in previous workflows, which are also present here.

This way can be used with A/B tests if we have different configurations for each trigger or optional steps.

It’s a flexible workflow, ideal for experimentation and the gradual adoption of new features, as well as conditional resource usage.

Challenges can be unpredictable testing behaviors and can become complex quickly.

Conclusion

Which strategy is better? It depends on several factors, including project size and complexity, team structure, performance requirements, and deployment needs.

The good approach is to start simple and grow based on your needs.

You can start with a simple linear workflow, then transition to a multi-job workflow for increased complexity, and finally begin using a reusable workflow for scaling.

Thanks for reading :)


GitHub Actions Strategy Guide was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Heba Aly


Print Share Comment Cite Upload Translate Updates
APA

Heba Aly | Sciencx (2025-07-30T04:06:04+00:00) GitHub Actions Strategy Guide. Retrieved from https://www.scien.cx/2025/07/30/github-actions-strategy-guide/

MLA
" » GitHub Actions Strategy Guide." Heba Aly | Sciencx - Wednesday July 30, 2025, https://www.scien.cx/2025/07/30/github-actions-strategy-guide/
HARVARD
Heba Aly | Sciencx Wednesday July 30, 2025 » GitHub Actions Strategy Guide., viewed ,<https://www.scien.cx/2025/07/30/github-actions-strategy-guide/>
VANCOUVER
Heba Aly | Sciencx - » GitHub Actions Strategy Guide. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/07/30/github-actions-strategy-guide/
CHICAGO
" » GitHub Actions Strategy Guide." Heba Aly | Sciencx - Accessed . https://www.scien.cx/2025/07/30/github-actions-strategy-guide/
IEEE
" » GitHub Actions Strategy Guide." Heba Aly | Sciencx [Online]. Available: https://www.scien.cx/2025/07/30/github-actions-strategy-guide/. [Accessed: ]
rf:citation
» GitHub Actions Strategy Guide | Heba Aly | Sciencx | https://www.scien.cx/2025/07/30/github-actions-strategy-guide/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.