CI/CD Pipeline with GitHub Actions
Build a multi-job CI/CD pipeline to prep for the GH-200 exam.
Introduction
30 Second Summary
Every time you push code, something needs to check it actually works. In professional teams, that job belongs to a CI/CD pipeline that automatically tests, builds, and reports on every change.
In this project, you will build a production-style GitHub Actions pipeline from scratch. You will practice the exact workflow patterns tested on the GH-200 certification exam.
What You'll Build
A multi-job CI/CD workflow for a Node.js project that tests across multiple versions, passes artifacts between jobs, and uses a custom composite action for reusable logic.
By the end of this project, you'll have:
- A multi-trigger workflow that responds to pushes, pull requests, and manual dispatches with scoped permissions and concurrency controls.
- A matrix testing strategy that runs tests across Node.js 20 and 22, uploads results as artifacts, and generates a summary report.
- A custom composite action that encapsulates reusable setup logic you can share across jobs.
- Secret Mission: a reusable workflow that other repositories can call to run your CI logic.
Are there any prerequisites?
You need a free GitHub account, Git installed on your machine, Node.js installed, and VS Code ready to go. No prior GitHub Actions experience is required.
Set Up the Repository and Node.js Project
Every CI/CD pipeline needs code to run on. Before you can write workflow files, you need a GitHub repository with a working project that can be built and tested.
In this step, you will create a public repository on GitHub, clone it to your local machine, and set up a minimal Node.js project with source code and tests. This gives your GitHub Actions pipeline something real to build, test, and report on.
In this step, get ready to:
- Create a public GitHub repository and clone it locally.
- Initialize a Node.js project with source code and automated tests.
Create a GitHub repository and clone it locally
GitHub Actions workflows run inside a GitHub repository. You need a public repo so that GitHub Actions runners are free and unlimited.
- Open your browser and go to github.com.
- In the upper-right corner, click the + menu, then click New repository.
- Type gh-actions-exam-lab in the Repository name field.
- Select Public for the repository visibility.
- Toggle Add a README file to On.
- Click Create repository.
Why public?
Public repositories get free and unlimited GitHub Actions minutes on standard GitHub-hosted runners. Private repos on the free plan are limited to 2,000 minutes per month.
GitHub will redirect you to your new repository page. Now you will clone it to your local machine.
- On your repository page, click the green Code button.
- Copy the HTTPS URL shown in the dropdown.
- Switch back to VS Code and open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone your repository by running this command:
git clone [[REPO_URL="https://github.com/YOUR_USERNAME/gh-actions-exam-lab.git"]]
- Move into the cloned folder by running:
cd gh-actions-exam-lab
- Open this folder in VS Code by running:
code .
What does this do?
The code . command opens the current directory in a new VS Code window. If VS Code asks whether you trust the authors of the folder, click Yes, I trust the authors.
- Confirm the clone worked by running:
ls
You should see README.md in the output. This file was created when you initialized the repository with a README.
Don't see README.md?
Make sure you ran cd gh-actions-exam-lab after cloning. If ls shows a folder called gh-actions-exam-lab instead of README.md, you are one directory level above. Run cd gh-actions-exam-lab and try again.
If the clone command itself failed, double-check that you pasted the correct HTTPS URL from GitHub.
Help me troubleshoot my git clone
Initialize the Node.js project files
Your pipeline needs a project it can build and test. You will create a minimal Node.js project with two utility functions and a test suite that uses Node's built-in test runner.
- In VS Code's terminal (inside your gh-actions-exam-lab folder), create the src directory by running:
mkdir src
- Create package.json in the project root. In the VS Code Explorer sidebar, click the new file icon next to GH-ACTIONS-EXAM-LAB.
- Name the file package.json and press Enter.
- Paste the following into package.json:
{
"name": "gh-actions-exam-lab",
"version": "1.0.0",
"description": "GitHub Actions GH-200 exam practice project",
"main": "src/index.js",
"scripts": {
"build": "echo \"Build complete\"",
"test": "node --test src/index.test.js"
},
"keywords": [],
"author": "",
"license": "MIT"
}
What does this code do?
The scripts section defines two commands your CI pipeline will call. build prints a success message (a placeholder for a real build step). test invokes Node's built-in test runner with node --test against your test file.
No external testing framework is needed. Node.js 20+ ships with a built-in test runner via the node:test module.
- Save package.json.
- Verify your build script works by running:
npm run build
You should see Build complete printed in the terminal.
Seeing a JSON parse error?
Check package.json for missing commas or mismatched quotes. JSON does not allow trailing commas after the last item in an object or array.
Help me fix my package.json
Now create the application source code.
- In the Explorer sidebar, click the new file icon next to the src folder.
- Name the file index.js and press Enter.
- Paste the following into src/index.js:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
What does this code do?
This file exports two simple utility functions: add and multiply. They exist to give your test suite something to verify. In a real project, these would be your application logic.
- Save src/index.js.
Next, create the test file that verifies these functions.
- Click the new file icon next to the src folder again.
- Name the file index.test.js and press Enter.
- Paste the following into src/index.test.js:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, multiply } = require('./index');
describe('add', () => {
it('adds two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('handles negative numbers', () => {
assert.strictEqual(add(-1, 1), 0);
});
});
describe('multiply', () => {
it('multiplies two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
});
it('handles zero', () => {
assert.strictEqual(multiply(5, 0), 0);
});
});
What does this code do?
This test file imports describe and it from the built-in node:test module and assert from node:assert. No external dependencies required.
Each describe block groups related tests. Each it block asserts one specific behavior using assert.strictEqual. If the actual value does not match the expected value, the test fails.
- Save src/index.test.js.
- Run the test suite to verify everything works:
npm test
You should see all 4 tests passing with checkmarks and a summary showing 4 pass, 0 fail.
Tests failing or command not found?
If you see a module-not-found error, confirm that src/index.js and src/index.test.js both exist. The require('./index') in the test file looks for index.js in the same src folder.
If you see a syntax error, check that your package.json test script is exactly node --test src/index.test.js. A typo in the file path will cause the runner to fail.
Help me debug my failing tests
- Generate a package-lock.json by running:
npm install
Why run npm install with no dependencies?
Even though this project has no external dependencies, running npm install generates a package-lock.json file. Your CI workflow will use npm ci (which requires a lockfile) to install dependencies in a clean, reproducible way.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is package.json:
{
"name": "gh-actions-exam-lab",
"version": "1.0.0",
"description": "GitHub Actions GH-200 exam practice project",
"main": "src/index.js",
"scripts": {
"build": "echo \"Build complete\"",
"test": "node --test src/index.test.js"
},
"keywords": [],
"author": "",
"license": "MIT"
}
Here is src/index.js:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
Here is src/index.test.js:
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { add, multiply } = require('./index');
describe('add', () => {
it('adds two numbers', () => {
assert.strictEqual(add(2, 3), 5);
});
it('handles negative numbers', () => {
assert.strictEqual(add(-1, 1), 0);
});
});
describe('multiply', () => {
it('multiplies two numbers', () => {
assert.strictEqual(multiply(3, 4), 12);
});
it('handles zero', () => {
assert.strictEqual(multiply(5, 0), 0);
});
});
Your repository has a working build and test setup. Next up, you will write your first GitHub Actions workflow to automate these commands every time you push code.
Author Your First Multi-Trigger Workflow
Your Node.js project is ready with source code, tests, and a build script. Now it's time to tell GitHub Actions what to do with it.
Every GitHub Actions pipeline starts with a workflow file that answers three questions: when should this run, what environment does it need, and what steps should it execute? In this step, you will author a complete workflow file that responds to multiple triggers and runs a build job.
In this step, get ready to:
- Create a workflow YAML file with three different triggers.
- Define a build job that checks out code, installs dependencies, and builds the project.
- Add security and optimization settings at the workflow level.
Create the workflow file with multi-trigger configuration
GitHub Actions workflows live in the .github/workflows directory of your repository. The file must use a .yml or .yaml extension.
- Create the required directory structure by running these commands in your terminal:
mkdir -p .github/workflows
- Create a new file called ci.yml inside .github/workflows/ and open it in VS Code.
- Add the workflow name and trigger configuration by pasting this code:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
What do these triggers do?
- push fires every time someone pushes a commit to the main branch.
- pull_request fires when a pull request targets main. This is how teams gate merges with automated checks.
- workflow_dispatch lets you trigger the workflow manually from the GitHub UI or API with a dropdown input for choosing an environment.
Why three triggers on one workflow?
The GH-200 exam tests your ability to combine triggers on a single workflow. In production, this pattern ensures code is validated on every push, every PR, and on-demand when a team member needs to run the pipeline manually.
- Save ci.yml.
YAML indentation errors?
YAML is whitespace-sensitive. Make sure you are using spaces (not tabs) and that each level is indented by exactly 2 spaces. The YAML Language Support extension you installed in Step 1 will underline errors in red.
Help me fix my YAML indentation
Add the build job with environment variables and contexts
The jobs section defines the actual work your workflow performs. Each job runs on a fresh virtual machine and executes a series of steps.
- In ci.yml, add the following code below the workflow_dispatch section you just wrote:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Display trigger info
run: |
echo "Event: ${{ github.event_name }}"
echo "Actor: ${{ github.actor }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
What does this job do?
- runs-on: ubuntu-latest tells GitHub to run this job on a fresh Ubuntu virtual machine.
- actions/checkout@v6 clones your repository into the runner so subsequent steps can access your code.
- actions/setup-node@v6 installs Node.js 22 and enables npm caching. Caching speeds up future runs by reusing downloaded packages.
- npm ci installs dependencies from your lock file (faster and more deterministic than npm install).
- The Display trigger info step uses GitHub contexts like github.event_name and github.actor to print metadata about what triggered the run.
- Save ci.yml.
Not sure where to paste?
The jobs: key should be at the root level (no indentation), directly below the last line of your on: block. Make sure there is a blank line separating them.
Help me position the jobs section correctly in my workflow file
Configure permissions and concurrency controls
Right now your workflow can run, but it has full default permissions and no protection against duplicate runs. The GH-200 exam covers both scoped permissions and concurrency as security best practices.
- In ci.yml, find the line that says jobs:.
- Add the following block directly above jobs:, with a blank line before and after:
permissions:
contents: read
actions: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_ENV: production
What do these settings do?
- permissions restricts the GITHUB_TOKEN to read-only access for contents and actions. This follows the principle of least privilege.
- concurrency groups runs by workflow name and branch. If you push two commits quickly, the second run cancels the first instead of wasting runner minutes.
- env sets NODE_ENV to production for all jobs in the workflow. Environment variables defined at the workflow level are available to every step.
- Save ci.yml.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete ci.yml file. Make sure yours matches exactly:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
permissions:
contents: read
actions: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_ENV: production
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Display trigger info
run: |
echo "Event: ${{ github.event_name }}"
echo "Actor: ${{ github.actor }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
- Commit and push your workflow to GitHub by running these commands:
git add .
git commit -m "Add CI workflow with multi-trigger config"
git push origin main
- Open your repository on GitHub in a browser.
- Click the Actions tab at the top of your repository page.
- You should see a workflow run named CI Pipeline in progress or completed with a green checkmark.
Why did the workflow run automatically?
Your push to main matched the push trigger you configured. GitHub detected the new workflow file and executed it immediately.
Don't see a workflow run?
Make sure your file is at the exact path .github/workflows/ci.yml. The directory name must be .github with a leading dot. Also confirm your push succeeded by checking the output of git push for errors.
Help me figure out why my GitHub Actions workflow isn't running
Your first workflow is live and responding to pushes. Next up, you will add matrix testing across multiple Node.js versions and pass data between jobs using artifacts.
Add Matrix Testing and Artifacts
Your build job compiles the project successfully, but it only runs on a single Node.js version. Real-world CI pipelines need to verify compatibility across multiple runtime versions and share data between jobs.
In this step, you will add parallel test runs across Node.js 20 and 22 using a matrix strategy, upload test results as artifacts, and create a reporting job that downloads those artifacts and generates a summary.
In this step, get ready to:
- Add a test job with matrix strategy and job dependencies.
- Upload test results as workflow artifacts.
- Add a report job that downloads artifacts and writes a step summary.
Add the test job with matrix strategy
A matrix strategy tells GitHub Actions to run the same job multiple times with different variable combinations. You will test across Node.js 20 and 22 to catch version-specific issues early.
- In VS Code, open .github/workflows/ci.yml.
- Find the closing lines of the build job (the Display trigger info step is the last step in that job).
- Add the following test job below the build job by pasting this code:
test:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
fail-fast: false
steps:
- uses: ./.github/actions/setup-node-project
with:
node-version: ${{ matrix.node-version }}
- name: Run tests
run: npm test 2>&1 | tee test-results.txt
What does this code do?
- needs: build creates a dependency so test only runs after build succeeds.
- The matrix block defines node-version: [20, 22] which spawns two parallel job instances, one per version.
- fail-fast: false ensures all matrix combinations run to completion even if one fails.
- The first step uses a custom composite action at ./.github/actions/setup-node-project (you will create this in the next step).
- npm test 2>&1 | tee test-results.txt runs the tests and writes output to both the terminal and a file for later upload.
What is a matrix strategy?
A matrix strategy automatically creates multiple job runs from a set of variables you define. On the GH-200 exam, you will see questions about matrix syntax, fail-fast behavior, and how to reference matrix values with ${{ matrix.<var> }}.
This job will fail until Step 4
The test job references ./.github/actions/setup-node-project which does not exist yet. When you push at the end of this step, the build job will pass but the test and report jobs will fail. This is expected. You will fix it in Step 4.
Upload test results as an artifact
Artifacts let you persist files from a job and share them with downstream jobs. The GH-200 exam tests your knowledge of artifact retention, naming, and conditional uploads.
- Still in .github/workflows/ci.yml, add the following step below the Run tests step (still inside the test job):
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-node-${{ matrix.node-version }}
path: test-results.txt
retention-days: 5
What does this code do?
- if: always() ensures the artifact uploads even if tests fail. Without this, a failing test would skip the upload.
- actions/upload-artifact@v4 saves the specified file as a downloadable artifact on the workflow run.
- The name uses the matrix variable to create unique artifact names like test-results-node-20 and test-results-node-22.
- retention-days: 5 automatically deletes the artifact after 5 days to save storage.
Add the report job
The report job downloads artifacts from all matrix runs and generates a markdown summary visible on the workflow run page. This pattern demonstrates cross-job data sharing, which is a core GH-200 exam topic.
- Below the test job (after its last step), add the following report job:
report:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
with:
pattern: test-results-*
merge-multiple: true
- name: Generate summary
run: |
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
for file in test-results*.txt; do
if [ -f "$file" ]; then
echo "| $file | Completed |" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "Triggered by: **${{ github.event_name }}** on \`${{ github.ref }}\`" >> $GITHUB_STEP_SUMMARY
What does this code do?
- needs: test waits for all matrix runs of the test job to complete before starting.
- if: always() at the job level ensures the report still runs even if some tests failed.
- actions/download-artifact@v4 with pattern: test-results-* matches all artifacts whose names start with that prefix.
- merge-multiple: true combines multiple matching artifacts into a single directory instead of nested folders.
- $GITHUB_STEP_SUMMARY is a special file path provided by GitHub Actions. Markdown written to it appears on the workflow run's summary page.
What is GITHUB_STEP_SUMMARY?
GitHub Actions provides the $GITHUB_STEP_SUMMARY environment file so jobs can publish rich markdown summaries without digging through logs. The summary renders on the workflow run's summary page in the GitHub UI, making test results and deployment info visible at a glance.
- Save .github/workflows/ci.yml.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file, then continue below to push your changes.
ⓧ I'd like to double check the full code
Here is the complete .github/workflows/ci.yml file at this point:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
permissions:
contents: read
actions: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_ENV: production
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Display trigger info
run: |
echo "Event: ${{ github.event_name }}"
echo "Actor: ${{ github.actor }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
test:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
fail-fast: false
steps:
- uses: ./.github/actions/setup-node-project
with:
node-version: ${{ matrix.node-version }}
- name: Run tests
run: npm test 2>&1 | tee test-results.txt
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-node-${{ matrix.node-version }}
path: test-results.txt
retention-days: 5
report:
needs: test
runs-on: ubuntu-latest
if: always()
steps:
- name: Download all test artifacts
uses: actions/download-artifact@v4
with:
pattern: test-results-*
merge-multiple: true
- name: Generate summary
run: |
echo "## Test Results Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
for file in test-results*.txt; do
if [ -f "$file" ]; then
echo "| $file | Completed |" >> $GITHUB_STEP_SUMMARY
fi
done
echo "" >> $GITHUB_STEP_SUMMARY
echo "Triggered by: **${{ github.event_name }}** on \`${{ github.ref }}\`" >> $GITHUB_STEP_SUMMARY
- Commit and push your changes by running these commands in your terminal:
git add .
git commit -m "Add test matrix and report jobs"
git push
- Open your repository on GitHub and click the **Actions** tab.
- Click the latest workflow run (triggered by your push).
- You should see the build job succeed. The test and report jobs will fail because the composite action does not exist yet.
Build job also failing?
- Make sure you did not accidentally modify the build job's indentation. YAML is whitespace-sensitive.
- Check that the test and report keys are at the same indentation level as build (two spaces under jobs:).
- Verify there are no trailing spaces or tab characters mixed in.
Why is my workflow YAML causing a parse error?
Your pipeline now tests across multiple Node.js versions and passes results between jobs using artifacts. Next up, you will create the custom composite action that the test job is wired to use.
Create a Custom Composite Action
Your CI pipeline now builds, tests across multiple Node.js versions, and generates a summary report. But look at the test job in your workflow. It references a composite action (./.github/actions/setup-node-project) that doesn't exist yet.
A composite action packages multiple workflow steps into a single reusable unit. Instead of repeating checkout, Node.js setup, and dependency installation in every job, you define them once and call the action wherever needed.
In this step, get ready to:
- Create the action.yml metadata file for your composite action.
- Define the composite steps that handle checkout, Node.js setup, and dependency installation.
- Push your changes and verify the full pipeline runs successfully on GitHub.
Create the action metadata file
Every composite action lives in its own directory with an action.yml file. This file declares the action's name, description, and any inputs it accepts.
- Create the directory structure for your composite action by running this command:
mkdir -p .github/actions/setup-node-project
- Confirm the directory was created by running:
ls .github/actions/setup-node-project
You should see an empty directory listed with no errors.
Seeing 'No such file or directory'?
Make sure you're running the command from your project root (the folder containing package.json). The -p flag creates all intermediate directories, so if it failed, you may have a typo in the path.
Help me figure out why my mkdir command failed.
- Create a new file called action.yml inside .github/actions/setup-node-project/ in VS Code.
- Paste the following metadata into action.yml:
name: 'Setup Node Project'
description: 'Checks out code, sets up Node.js with caching, and installs dependencies'
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '22'
What does this metadata do?
- name and description are required fields that identify your action in workflow logs and the Actions UI.
- inputs declares parameters that callers can pass in. Here, node-version defaults to '22' but can be overridden by the calling job.
- required: false means the input is optional. If the caller doesn't pass it, the default value applies.
Define the composite steps
The runs section is where you define what your composite action actually does. Each step works exactly like a step in a regular workflow job.
- Add the following runs section below the inputs block in action.yml:
runs:
using: 'composite'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
What do these composite steps do?
- using: 'composite' tells GitHub this action contains multiple steps (not a Docker or JavaScript action).
- The first step checks out the repository code using actions/checkout@v6.
- The second step sets up Node.js using the version passed via ${{ inputs.node-version }} and enables npm caching.
- The third step runs npm ci to install dependencies. Composite actions require an explicit shell field on every run step.
Why composite over JavaScript or Docker actions?
Composite actions are the simplest action type to author because they reuse the same YAML syntax you already know from workflows. JavaScript actions require building and bundling code, while Docker actions add container overhead.
For the GH-200 exam, composite actions appear in Domain 3 (Author and Maintain Actions) and are the most commonly tested action type because they demonstrate input passing, step composition, and the shell requirement.
- Save action.yml.
YAML indentation issues?
YAML is sensitive to indentation. Make sure runs is at the same indent level as name and inputs (no indentation). Each step under steps should be indented with 4 spaces and start with a dash.
Help me fix my action.yml indentation.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
name: 'Setup Node Project'
description: 'Checks out code, sets up Node.js with caching, and installs dependencies'
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '22'
runs:
using: 'composite'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
Push and verify the full pipeline
Your ci.yml workflow already references this composite action in the test job. Now that the action file exists, pushing to main triggers the full pipeline.
- In ci.yml, find the test job's first step to confirm it already uses your composite action:
steps:
- uses: ./.github/actions/setup-node-project
with:
node-version: ${{ matrix.node-version }}
How does the workflow find a local action?
The uses: ./.github/actions/setup-node-project path tells GitHub to look inside this repository for the action definition. GitHub resolves this path from the repository at the triggering commit ref, then executes the steps defined in action.yml.
The node-version: ${{ matrix.node-version }} line passes the matrix value into the composite action's input, which flows through to actions/setup-node@v6.
- Commit and push your changes by running these commands:
git add .
git commit -m "Add custom composite action"
git push
- Switch to your browser and open your repository on GitHub.
- Click the Actions tab.
- Click on the latest workflow run (Add custom composite action).
You should see three jobs: build, test (with two matrix entries), and report. All three should complete with green checkmarks.
Seeing a workflow failure?
Click on the failed job to see the error log. Common issues:
- "Can't find 'action.yml'" means the file path or name is wrong. Verify the file is at .github/actions/setup-node-project/action.yml (not action.yaml or a different folder name).
- "Invalid action" usually means a YAML syntax error in your action.yml. Check that using: 'composite' is nested under runs and each run step has a shell field.
Help me debug my composite action workflow failure.
Your composite action is working. Any job in this repository can now call uses: ./.github/actions/setup-node-project to get a fully configured Node.js environment in a single step. Next up, you'll take reusability even further with a Secret Mission.
Secret mission
Add a Reusable Workflow
Your CI pipeline works great for this repository. But what if other teams want the same test logic without copying and pasting your workflow? Reusable workflows let you define CI logic once and call it from any repository. This directly tests Domain 2 of the GH-200 exam.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on GitHub-hosted runners with a public repository, so there are no ongoing costs. Decide whether to keep your resources available, pause them to come back later, or delete them entirely.
Resources you used:
- GitHub repository with GitHub Actions workflows.
- Local project folder on your Desktop.
- YAML Language Support by Red Hat extension in VS Code.
Keep everything running
No action needed. Choose this if you're still actively studying for the GH-200 exam or want to keep experimenting with your workflows.
- Your repository stays on GitHub with all workflow history intact.
- GitHub Actions for public repos is free with unlimited minutes, so nothing will incur charges.
- Your local project folder and VS Code extension remain available for future practice.
Pause - I'll come back to this later
Disable your workflows so they stop triggering on pushes, but keep the repository and files intact for later.
- Open your repository on GitHub and click the **Actions** tab.
- In the left sidebar, click the **CI Pipeline** workflow.
- Click the dropdown menu (three dots) and select **Disable workflow**.
- Repeat for the **Call Reusable CI** workflow if you completed the Secret Mission.
- When you're ready to resume, return to the **Actions** tab, select the workflow, and click **Enable workflow**.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
Delete the GitHub repository:
- Open your repository on GitHub.
- Click **Settings** (if you cannot see the Settings tab, open the dropdown menu and click **Settings**).
- Scroll down to the **Danger Zone** section.
- Click **Delete this repository**.
- Click **I want to delete this repository**, then click **I have read and understand these effects**.
- Type the repository name to confirm and click **Delete this repository**.
Delete the local project folder:
- Open your terminal and run the appropriate command for your operating system.
On macOS/Linux:
rm -rf ~/Desktop/gh-actions-exam-lab
On Windows:
Remove-Item -Recurse -Force ~\Desktop\gh-actions-exam-lab
Uninstall the VS Code extension:
- In VS Code, click the **Extensions** icon in the left sidebar.
- Search for `YAML` and find the **YAML Language Support by Red Hat** extension.
- Click **Uninstall**.
Nice Work!
Nice Work!
Nice work! You just built a production-style CI/CD pipeline using GitHub Actions from scratch. This pipeline covers the exact workflow authoring patterns tested on the GH-200 certification exam.
You've learned how to:
- Author a multi-job CI workflow with push, pull_request, and workflow_dispatch triggers, scoped permissions, and concurrency controls.
- Use a matrix strategy to test across multiple Node.js versions, and pass data between jobs using artifacts and step summaries.
- Create a custom composite action with action.yml metadata to encapsulate reusable setup logic.
- (Secret Mission) Built a reusable workflow with workflow_call and a caller workflow that invokes it.
Ready to quiz yourself?