Continuous integration means every change is built and tested automatically before it can merge, so broken code is found in minutes instead of at release time. GitHub Actions runs workflows defined in YAML inside your repository, on GitHub-hosted or self-hosted runners.
Versions in use on this date. actions/checkout@v3 and actions/setup-node@v3 (both run on the Node 16 action runtime, with built-in dependency caching), Node.js 16 (LTS), and OIDC federation to AWS, which GitHub supports since late 2021. ubuntu-latest currently maps to Ubuntu 20.04; Ubuntu 22.04 runners are in beta.
Pipeline
flowchart TB
PR[Pull request or push to main] --> W[Workflow starts<br/>concurrency cancels older runs]
W --> T[Job test<br/>matrix Node 16 and 18]
W --> L[Job lint and scan]
T --> BI
L --> BI[Job build image<br/>needs test and lint]
BI -->|main branch only| OIDC[Assume AWS role via OIDC<br/>no stored keys]
OIDC --> ECR[Push image to Amazon ECR]
ECR --> DEP[Update deployment config<br/>GitOps pull request or environment approval]
T -->|artifacts| ART[Test reports and coverage]
A complete workflow
.github/workflows/ci.yml:
name: ci
on:
pull_request:
push:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [16, 18]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run lint
- run: npm test -- --ci --coverage
- uses: actions/upload-artifact@v3
if: always()
with: { name: coverage-${{ matrix.node }}, path: coverage/ }
image:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
id-token: write # required for OIDC
contents: read
env:
REGISTRY: 111111111111.dkr.ecr.ap-south-1.amazonaws.com
REPO: shop/api
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: arn:aws:iam::111111111111:role/github-ci-shop
aws-region: ap-south-1
- uses: aws-actions/amazon-ecr-login@v1
- run: |
docker build -t $REGISTRY/$REPO:${{ github.sha }} .
docker push $REGISTRY/$REPO:${{ github.sha }}
Highlights:
permissionsat the top sets theGITHUB_TOKENto read-only. Grant more per job only where needed (for exampleid-token: write).concurrencycancels older runs for the same branch, saving minutes.matrixruns the tests on several versions at once;fail-fast: falseshows all failures.cache: npmin setup-node restores dependencies keyed on the lockfile; useactions/cachefor other tools.needsandifcontrol order and when the image is pushed (only frommain).
Keyless AWS access with OIDC
Long-lived AWS_ACCESS_KEY_ID secrets in a repository are a standing risk. With OIDC, GitHub presents a signed token for each run and AWS exchanges it for temporary credentials.
- In IAM, create an identity provider for
https://token.actions.githubusercontent.com(audiencests.amazonaws.com). - Create a role with a trust policy limited to your repository and branch:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
"StringLike": { "token.actions.githubusercontent.com:sub": "repo:acme/shop-api:ref:refs/heads/main" }
}
}]
}
- Give the role only the permissions it needs (push to one ECR repository). The
subcondition is critical: without it any repository could assume the role.
Secrets, environments and approvals
- Store secrets in Settings, Secrets and variables. Secrets are not passed to workflows from forked pull requests.
- Use Environments (
staging,production) with required reviewers and environment-scoped secrets for deployment jobs. - Treat third-party actions as code that runs with your permissions: prefer official actions, pin to a version tag, and for sensitive workflows to a full commit SHA. Dependabot can keep them updated (
.github/dependabot.yml, ecosystemgithub-actions). - Never print secrets or run workflows with
pull_request_targeton untrusted code without understanding the risk.
Reuse and speed
- Reusable workflows (
workflow_call) share a standard pipeline across repositories. - Cache Docker layers with
docker/build-push-actionandcache-from: type=gha. - Run slow tests in parallel jobs, and split by path with
paths:filters to skip unrelated work. - Hosted runners are billed by minute beyond the free allowance; self-hosted runners suit private networks but need hardening and patching.
Debugging
- Read the failing step’s log; re-run with debug logging by setting the secret
ACTIONS_STEP_DEBUGtotrue. - Reproduce locally with
act(approximate), or run the same commands in a container. - Common failures: a missing
id-token: writepermission (OIDC), cache misses after lockfile changes, and differences between local and runner versions (pin them).