Build a CI/CD pipeline with GitHub Actions: a tested YAML workflow, secure OIDC deployment, approvals, caching and common mistakes.
In this article
To build a CI/CD pipeline with GitHub Actions, you add a YAML file under .github/workflows/ in your repository that tells GitHub when to run (for example on every pull request), what to run (install, test, build) and, for the deployment half, where to send the result. Continuous integration (CI) checks every change automatically. Continuous delivery or deployment (CD) ships the verified build to an environment. A basic working pipeline takes about an hour to set up and pays for itself within weeks.
This guide builds one step by step for a Node.js web application, then shows how to add a secure deployment job. The same structure applies to other languages; only the setup and test commands change.
Core concepts in GitHub Actions
- Workflow: a YAML file describing an automated process.
- Event: what triggers it, such as
push,pull_requestor a schedule. - Job: a set of steps that runs on one runner. Jobs run in parallel unless you say otherwise.
- Step: a single command or a reusable action.
- Runner: the machine that executes the job, either GitHub-hosted or self-hosted.
- Action: a reusable unit of automation, such as checking out code or setting up a language runtime.
- Secrets and variables: configuration stored in GitHub rather than in the repository.
CloudWiss lists GitHub Actions alongside Jenkins and GitLab CI among its pipeline tools. The ideas below transfer to all three.
Step 1: decide what the pipeline must prove
Before writing YAML, agree what "good" means for your project. A sensible baseline is: dependencies install cleanly from the lockfile, linting passes, tests pass, and the application builds. Add security checks and deployment afterwards. A pipeline that checks too little gives false comfort; one that takes thirty minutes gets bypassed. Aim for feedback in a few minutes.
Step 2: create the CI workflow
Create .github/workflows/ci.yml:
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
What each part is doing:
onruns the workflow for every pull request and for every push tomain.permissions: contents: readgives the job's token the minimum rights it needs. Start restrictive and widen only when required.concurrencycancels an older run for the same branch when you push again, saving time and runner minutes.timeout-minutesstops a hung job from running for hours.cache: npmcaches downloaded packages between runs, andnpm ciinstalls exactly what the lockfile specifies.- The artefact upload keeps the build output so the deployment job can reuse it instead of rebuilding.
Commit this file, open a pull request and watch the Actions tab. If a step fails, the log shows exactly which command and why.
Step 3: make the checks required
A pipeline nobody has to respect is decoration. In the repository settings, add a branch protection rule or ruleset for main that requires the CI job to pass and at least one review before merging. This is where continuous integration becomes a real quality gate.
Step 4: add deployment
For deployment, avoid long-lived cloud keys stored as secrets. GitHub Actions can authenticate to AWS and Azure through OpenID Connect (OIDC), receiving short-lived credentials for each run. Here is an AWS example, added as a second job in the same file (or in a separate deploy.yml):
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Download build output
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: eu-west-1
- name: Deploy
run: ./scripts/deploy.sh
Key points:
needs: testmeans deployment only runs after the tests succeed.- The
ifcondition limits deployment to pushes tomain, never to pull requests from forks. environment: productionlets you add required reviewers and environment-specific secrets in GitHub, giving a manual approval step before production changes.id-token: writeallows the OIDC token request. The role in AWS must have a trust policy that only accepts your repository (and ideally your branch or environment). The account number above is a placeholder.deploy.shis your own script, whether it syncs static files, updates a container service or applies Terraform. Keep it in the repository so it is reviewed too.
If you deploy containers, add a build-and-push job that creates an image tagged with the commit SHA, then deploys that exact tag. The image-based approach pairs well with blue-green and canary deployment strategies. For the container basics, see Docker explained for business teams.
Step 5: harden the pipeline
- Pin third-party actions. Actions from unknown publishers run with access to your build. Prefer official or well-known actions and consider pinning to a full commit SHA for anything sensitive.
- Keep secrets out of logs. GitHub masks registered secrets, but a derived value may not be masked. Do not echo credentials.
- Treat pull requests from forks as untrusted. Secrets are not passed to them by default; do not weaken that. Be especially careful with the
pull_request_targettrigger. - Enable dependency updates. Dependabot can open pull requests for outdated actions and packages.
- Scan. Add dependency and image vulnerability scanning, and secret scanning on the repository.
Step 6: keep it fast and reliable
- Cache dependencies and, for containers, build layers.
- Run independent jobs, such as lint and tests, in parallel.
- Use a matrix when you must test several runtime versions, but avoid a matrix by default.
- Quarantine flaky tests. A pipeline that fails randomly teaches people to click "re-run" and ignore it.
- Reuse logic with reusable workflows once several repositories share the same steps.
Common mistakes
- Deploying from a developer's laptop as well. Two routes to production means neither is trustworthy. Make the pipeline the only way.
- Rebuilding for each environment. Build once, then promote the same artefact through staging to production.
- Long-lived cloud keys in secrets. Use OIDC and a narrowly scoped role.
- Overly broad
permissions. Set them at workflow or job level. - Using
npm installin CI. It can change dependency versions. Usenpm cior your ecosystem's lockfile-respecting equivalent. - No rollback plan. Know how to redeploy the previous artefact quickly.
- Infrastructure changes by hand. Put environment changes through the same pipeline using Terraform.
- No visibility after deployment. A green pipeline does not mean a healthy service. Pair it with monitoring and alerting.
Pipeline checklist
- Workflow runs on pull requests and on
main. - Lint, test and build complete in a few minutes.
- Branch protection requires the checks and a review.
- Permissions are minimal and actions are from trusted sources.
- Deployment uses OIDC, an environment and, for production, an approval.
- The same artefact is promoted between environments.
- Rollback has been tried at least once.
Summary
A good pipeline is small, fast and strict: prove the change is safe, build it once, deploy it the same way every time. Start with the CI job above and add deployment when you trust it. If you want help designing pipelines on GitHub Actions, GitLab CI or Jenkins, CloudWiss's cloud and DevOps services cover CI/CD pipeline automation from the first workflow through to production deployment.
Share this article
General educational information, not legal or professional advice for your specific situation.
