
Github Actions
- 94 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
github-actions is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- github-actions
- AI & Agent Building
- AI-coding skill
Github Actions by the numbers
- 94 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,644 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill github-actionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 94 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
GitHub Actions
Overview
GitHub Actions is a CI/CD platform that automates build, test, and deployment pipelines directly from GitHub repositories. Workflows are YAML files in .github/workflows/ triggered by events like pushes, pull requests, schedules, or manual dispatch. Each workflow contains one or more jobs that run on GitHub-hosted or self-hosted runners.
When to use: Automated testing, continuous deployment, release automation, scheduled tasks, multi-platform builds, dependency updates, container publishing, code quality checks, security scanning.
When NOT to use: Long-running services (use a proper hosting platform), heavy compute tasks exceeding runner limits (6-hour job timeout), tasks requiring persistent state between runs (use external storage), real-time event processing (use webhooks with a server).
Quick Reference
| Pattern | Syntax / Action | Key Points |
|---|---|---|
| Push trigger | on: push: branches: [main] | Filter by branch, path, or tag |
| PR trigger | on: pull_request: types: [opened, synchronize] | Defaults to opened, synchronize, reopened |
| Scheduled trigger | on: schedule: - cron: '0 6 * * 1' | UTC only, minimum 5-minute interval |
| Manual trigger | on: workflow_dispatch: inputs: | Define typed inputs for manual runs |
| Job dependencies | needs: [build, test] | Run jobs in sequence or parallel |
| Conditional job | if: github.ref == 'refs/heads/main' | Expression-based job/step filtering |
| Matrix strategy | strategy: matrix: node: [18, 20, 22] | Generates jobs for each combination |
| Dependency cache | actions/cache@v5 | Hash-based keys with restore-keys fallback |
| Setup with cache | actions/setup-node@v6 with cache: 'pnpm' | Built-in caching for package managers |
| Upload artifact | actions/upload-artifact@v4 | Share data between jobs or preserve outputs |
| Download artifact | actions/download-artifact@v4 | Retrieve artifacts from earlier jobs |
| Reusable workflow | uses: ./.github/workflows/reusable.yml | Called with workflow_call trigger |
| Composite action | action.yml with using: composite | Bundle multiple steps into one action |
| Concurrency | concurrency: group: ${{ github.ref }} | Cancel or queue duplicate runs |
| Environment secrets | ${{ secrets.API_KEY }} | Scoped to repo, org, or environment |
| OIDC authentication | permissions: id-token: write | Short-lived tokens for cloud providers |
| Step outputs | echo "key=value" >> "$GITHUB_OUTPUT" | Pass data between steps and jobs |
| Service containers | services: postgres: image: postgres:16 | Sidecar containers for integration tests |
| Timeout | timeout-minutes: 30 | Fail fast on hung jobs or steps |
| Attestations | actions/attest-build-provenance@v3 | SLSA build provenance for supply chain |
Expressions and Contexts
| Context | Example | Description |
|---|---|---|
github | github.ref_name, github.sha | Event metadata, repo info, actor |
env | env.NODE_ENV | Environment variables at current scope |
secrets | secrets.API_KEY | Encrypted secrets (masked in logs) |
inputs | inputs.environment | Workflow dispatch or reusable inputs |
matrix | matrix.node | Current matrix combination values |
steps | steps.build.outputs.version | Outputs from previous steps |
needs | needs.prepare.outputs.tag | Outputs from dependent jobs |
runner | runner.os, runner.arch | Runner environment info |
vars | vars.DEPLOY_URL | Repository or org configuration variables |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Using outdated action major versions | Pin to current major version (@v6) or commit SHA |
Missing persist-credentials: false | Set on checkout when using custom tokens or OIDC |
Broad permissions at workflow level | Set permissions: {} at workflow level, grant per-job |
| Cache key without dependency file hash | Include hashFiles('**/pnpm-lock.yaml') in cache key |
Secrets in if: conditions | Secrets cannot be used in if: expressions directly |
Using pull_request_target carelessly | Never run PR code with write permissions from pull_request_target |
| Not cancelling stale runs | Use concurrency with cancel-in-progress: true |
| Storing structured data as a single secret | Create individual secrets per value for proper log redaction |
| Referencing action tags without SHA pinning | Pin third-party actions to full commit SHA for supply chain safety |
| Hardcoding runner OS in scripts | Use runner.os context for cross-platform compatibility |
Using actions/cache without restore-keys | Always provide restore-keys for partial cache matches |
Interpolating user input in run: blocks | Pass untrusted values through env: to prevent script injection |
No timeout-minutes on jobs | Set explicit timeouts to fail fast on hung processes |
Using always() without scoping | Combine with status checks: if: always() && steps.x.outcome == 'success' |
Delegation
- Workflow debugging: Use
Exploreagent to inspect workflow run logs - Security auditing: Use
Taskagent to review permissions and secret usage - Code review: Delegate to
code-revieweragent for workflow PR reviews
References
- Workflow syntax, triggers, jobs, steps, and concurrency
- Caching strategies and artifact management
- Matrix strategies, reusable workflows, and composite actions
- Security, secrets, OIDC, and permissions hardening
Caching and Artifacts
Dependency Caching with Setup Actions
The simplest approach uses built-in caching in setup actions. This handles cache key generation and restore automatically.
Node.js with pnpm
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfileNode.js with npm
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: 'npm'
- run: npm ciNode.js with yarn
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: 'yarn'
- run: yarn install --immutablePython with pip
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- run: pip install -r requirements.txtGo
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- run: go build ./...Rust
steps:
- uses: actions/checkout@v6
- uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-Manual Caching with actions/cache
Use actions/cache@v5 when setup actions do not cover your use case or you need fine-grained control.
steps:
- uses: actions/checkout@v6
- name: Cache node_modules
id: cache-deps
uses: actions/cache@v5
with:
path: node_modules
key: deps-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
deps-${{ runner.os }}-
- name: Install dependencies
if: steps.cache-deps.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfileCache Key Design
Build keys from most-specific to least-specific:
key: deps-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
deps-${{ runner.os }}-- Exact match: Full key hit restores the exact cache
- Prefix match:
restore-keysfinds the most recent partial match - Cache miss: No match found, full install required
Multiple Cache Paths
- uses: actions/cache@v5
with:
path: |
~/.pnpm-store
node_modules
.next/cache
key: all-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('src/**') }}
restore-keys: |
all-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
all-${{ runner.os }}-Granular Save and Restore
Use separate restore and save actions for advanced patterns like always saving regardless of job outcome:
steps:
- uses: actions/cache/restore@v4
with:
path: .build-cache
key: build-${{ runner.os }}-${{ github.sha }}
restore-keys: |
build-${{ runner.os }}-
- run: pnpm build
- uses: actions/cache/save@v4
if: always()
with:
path: .build-cache
key: build-${{ runner.os }}-${{ github.sha }}Cache Limits and Behavior
- 10 GB per repository total cache storage
- Caches are immutable once created (same key cannot be overwritten)
- 7-day eviction for caches not accessed within 7 days
- Branch scope: Workflow runs can restore caches from the current branch or the default branch
- Rate limit: 200 cache uploads per minute per repository
Artifacts
Artifacts persist data after a workflow completes. Use artifacts to share data between jobs or preserve build outputs.
Upload Artifact
steps:
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
if-no-files-found: errorDownload Artifact (Same Workflow)
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: pnpm build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- run: ./deploy.sh dist/Multiple Artifacts
steps:
- uses: actions/download-artifact@v4
with:
path: all-artifacts/
merge-multiple: trueWhen merge-multiple is true, all artifacts are downloaded and merged into the specified path.
Artifact vs Cache
| Feature | Cache | Artifact |
|---|---|---|
| Purpose | Speed up dependency installs | Preserve outputs between jobs |
| Lifetime | 7 days (last accessed) | Configurable retention (1-90d) |
| Size limit | 10 GB per repo | Varies by plan |
| Cross-workflow | Yes (same branch or default) | Same workflow run only |
| Mutable | No (key-based) | Yes (same name overwrites) |
| Post-run access | No | Yes (downloadable from UI/API) |
Build Output Caching
Next.js Build Cache
steps:
- uses: actions/cache@v5
with:
path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('src/**') }}
restore-keys: |
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-
nextjs-${{ runner.os }}-
- run: pnpm buildTurborepo Remote Cache
steps:
- run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}Docker Layer Caching
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: app:latest
cache-from: type=gha
cache-to: type=gha,mode=maxThe type=gha backend uses GitHub Actions cache for Docker layer caching.
Matrix and Reusable Workflows
Matrix Strategy
Matrix strategies generate multiple job runs from variable combinations.
Basic Matrix
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: npm testThis generates 6 jobs (3 Node versions x 2 operating systems).
Include and Exclude
strategy:
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
include:
- node: 22
os: ubuntu-latest
coverage: true
exclude:
- node: 18
os: windows-latest- `include`: Adds properties to matching combinations or creates new combinations
- `exclude`: Removes specific combinations from the matrix
Include-Only Matrix
strategy:
matrix:
include:
- name: Unit Tests
command: test:unit
- name: Integration Tests
command: test:integration
- name: E2E Tests
command: test:e2eWhen only include is used without top-level variables, each entry becomes a standalone job.
Fail-Fast and Max Parallel
strategy:
fail-fast: false
max-parallel: 3
matrix:
node: [18, 20, 22]- `fail-fast: true` (default): Cancels all in-progress matrix jobs when any job fails
- `fail-fast: false`: Lets all jobs run to completion regardless of failures
- `max-parallel`: Limits concurrent matrix jobs (useful for rate-limited resources)
Dynamic Matrix with fromJSON
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
echo 'matrix={"node":[18,20,22],"include":[{"node":22,"coverage":true}]}' >> "$GITHUB_OUTPUT"
test:
needs: prepare
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: npm testUse fromJSON() to generate matrix values dynamically from a previous job.
Reusable Workflows
Reusable workflows let you define a workflow once and call it from other workflows.
Defining a Reusable Workflow
# .github/workflows/reusable-test.yml
name: Reusable Test
on:
workflow_call:
inputs:
node-version:
type: string
required: false
default: '22'
working-directory:
type: string
required: false
default: '.'
secrets:
npm-token:
required: false
outputs:
coverage:
description: 'Coverage percentage'
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.coverage.outputs.value }}
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- run: npm test -- --coverage
- name: Extract coverage
id: coverage
run: echo "value=$(jq '.total.lines.pct' coverage/coverage-summary.json)" >> "$GITHUB_OUTPUT"Calling a Reusable Workflow
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
test-app:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '22'
working-directory: ./app
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
test-lib:
uses: ./.github/workflows/reusable-test.yml
with:
working-directory: ./lib
report:
needs: [test-app, test-lib]
runs-on: ubuntu-latest
steps:
- run: |
echo "App coverage: ${{ needs.test-app.outputs.coverage }}%"
echo "Lib coverage: ${{ needs.test-lib.outputs.coverage }}%"Cross-Repository Reusable Workflows
jobs:
deploy:
uses: my-org/shared-workflows/.github/workflows/deploy.yml@v1
with:
environment: production
secrets: inherit- Reference with
owner/repo/.github/workflows/file.yml@ref secrets: inheritpasses all caller secrets to the reusable workflow- The
@refcan be a branch, tag, or commit SHA
Calling with Matrix
jobs:
test:
strategy:
matrix:
package: [app, lib, docs]
uses: ./.github/workflows/reusable-test.yml
with:
working-directory: ./packages/${{ matrix.package }}Matrix strategies work with reusable workflow calls. The output from the last successful matrix job is used when the reusable workflow sets outputs.
Reusable Workflow Limits
- Maximum 4 levels of nesting (workflow calling workflow calling workflow...)
- Maximum 20 reusable workflows per workflow file
envcontext variables set at the caller level are not propagated to the called workflow- Reusable workflows from public repos can be used by any repo; private repos can only use workflows within the same repo or organization
Composite Actions
Composite actions bundle multiple steps into a single reusable action.
Creating a Composite Action
# .github/actions/setup-project/action.yml
name: Setup Project
description: 'Install pnpm and project dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '22'
outputs:
cache-hit:
description: 'Whether the cache was hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
shell: bash
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: 'pnpm'
- name: Install dependencies
id: cache
run: pnpm install --frozen-lockfile
shell: bashEvery run step in a composite action requires an explicit shell property.
Using a Composite Action
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-project
with:
node-version: '20'
- run: pnpm testComposite vs Reusable Workflow
| Feature | Composite Action | Reusable Workflow |
|---|---|---|
| Granularity | Steps within a job | Entire jobs |
| Secrets access | Passed as inputs only | secrets: or secrets: inherit |
| Debug visibility | Appears as one step in logs | Each step visible separately |
| Services | Cannot define services | Can define service containers |
| Matrix | Cannot define matrix | Can use matrix strategy |
| Calling syntax | uses: in a step | uses: at the job level |
| Location | Any directory with action.yml | Must be in .github/workflows/ |
Security and Secrets
Secrets Management
Secret Scopes
| Scope | Access | Use Case |
|---|---|---|
| Repository | Single repo workflows | API keys for one project |
| Organization | Selected or all repos in org | Shared service credentials |
| Environment | Jobs targeting that environment | Production-only deploy keys |
Using Secrets in Workflows
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}Secrets are masked in logs automatically. GitHub redacts exact string matches, so avoid storing structured data (JSON, YAML) as a single secret. Create individual secrets per value instead.
Secret Restrictions
- Secrets cannot be used in
if:conditionals directly - Any user with write access to a repo has read access to all repo-level secrets
- Secrets are not passed to workflows triggered by forks (except
pull_request_target) - Maximum 100 organization secrets, 100 repository secrets, 100 environment secrets
Managing Secrets via CLI
# Set a repository secret
gh secret set API_KEY --body "sk-abc123"
# Set from a file
gh secret set DEPLOY_KEY < deploy-key.pem
# Set an environment secret
gh secret set API_KEY --env production --body "sk-prod-abc123"
# List secrets
gh secret listEnvironment Protection Rules
Environments add deployment safeguards with required reviewers, wait timers, and branch restrictions.
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: ./deploy.sh productionProtection Rule Options
| Rule | Effect |
|---|---|
| Required reviewers | Pauses workflow until approved (up to 6 reviewers) |
| Wait timer | Delays job start by 0-43200 minutes |
| Branch restrictions | Limits which branches can deploy to the environment |
| Custom rules | Org-level deployment protection via GitHub Apps |
Environment-Scoped Secrets
Environment secrets override repository secrets of the same name. Use this to have different credentials per environment:
jobs:
deploy:
environment: ${{ inputs.environment }}
steps:
- run: ./deploy.sh
env:
API_URL: ${{ secrets.API_URL }}The value of API_URL comes from the environment secret, not the repository secret.
Permissions Hardening
Principle of Least Privilege
permissions: {}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm build
publish:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm publishSet permissions: {} at the workflow level to start with no permissions. Grant only what each job needs.
Common Permission Sets
| Task | Required Permissions |
|---|---|
| Checkout code | contents: read |
| Push commits/tags | contents: write |
| Create/update PRs | pull-requests: write |
| Publish packages | packages: write |
| Deploy to Pages | pages: write, id-token: write |
| OIDC cloud authentication | id-token: write |
| Update check runs | checks: write |
| Post PR comments | pull-requests: write |
| Upload security results | security-events: write |
GITHUB_TOKEN Default Permissions
Organizations created before February 2023 may have read-write as the default. Change to read in repository or organization settings under Actions > General > Workflow permissions.
OIDC Authentication
OIDC eliminates long-lived cloud credentials by issuing short-lived tokens per workflow run.
How It Works
1. Workflow requests an OIDC token from GitHub's identity provider 2. Cloud provider validates the token against configured trust policy 3. Cloud provider issues short-lived access credentials 4. Workflow uses temporary credentials for cloud operations
AWS with OIDC
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: us-east-1
- run: aws s3 sync dist/ s3://my-bucket/Google Cloud with OIDC
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123/locations/global/workloadIdentityPools/pool/providers/github
service_account: deploy@project.iam.gserviceaccount.com
- uses: google-github-actions/deploy-cloudrun@v2
with:
service: my-app
region: us-central1
image: gcr.io/project/app:latestAzure with OIDC
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: azure/webapps-deploy@v3
with:
app-name: my-appOIDC Token Claims
The OIDC token includes claims that cloud providers use for authorization:
| Claim | Example | Use For |
|---|---|---|
sub | repo:org/repo:ref:refs/heads/main | Branch-level access |
repository | org/repo | Repo-level access |
environment | production | Environment-level access |
actor | username | User-level access |
ref | refs/heads/main | Branch filtering |
Supply Chain Security
Pin Actions to Commit SHA
# Vulnerable - tag can be moved to malicious code
- uses: actions/checkout@v6
# Secure - pinned to exact commit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1Pin third-party actions to full commit SHAs. Use Dependabot to keep pinned SHAs updated:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weeklyAvoid pull_request_target Pitfalls
# DANGEROUS - runs PR code with write permissions
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm testpull_request_target runs with the base branch context and write permissions. Never checkout and execute code from the PR head in this context. Use pull_request for running untrusted code.
Safe pull_request_target Pattern
on: pull_request_target
jobs:
label:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5Only use pull_request_target for operations that do not execute PR code, such as labeling or commenting.
Script Injection Prevention
# Vulnerable - untrusted input in shell
- run: echo "Title is ${{ github.event.pull_request.title }}"
# Safe - pass through environment variable
- run: echo "Title is $TITLE"
env:
TITLE: ${{ github.event.pull_request.title }}Never interpolate user-controlled values (github.event.*.title, github.event.*.body) directly in run: blocks. Pass them through environment variables instead.
Security Scanning Tools
| Tool | Purpose | Integration |
|---|---|---|
| Dependabot | Dependency vulnerability scanning | Built-in, .github/dependabot.yml |
| CodeQL | Static analysis for security bugs | github/codeql-action |
| Trivy | Container and filesystem scanning | aquasecurity/trivy-action |
| zizmor | GitHub Actions static analysis | Checks workflow misconfigs |
Workflow Syntax
File Location and Naming
Workflow files must be stored in .github/workflows/ with a .yml or .yaml extension. Each file defines one workflow.
# .github/workflows/ci.yml
name: CI
run-name: CI for ${{ github.ref_name }}Triggers
Push and Pull Request
on:
push:
branches: [main, 'release/**']
paths: ['src/**', 'package.json']
tags: ['v*']
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
paths-ignore: ['docs/**', '*.md']Path filters reduce unnecessary runs. Use paths to include or paths-ignore to exclude. Do not use both on the same event.
Schedule
on:
schedule:
- cron: '0 6 * * 1-5'Cron expressions use UTC. Minimum interval is 5 minutes. Scheduled workflows run on the default branch only. During high-load periods, runs may be delayed or skipped.
Manual Dispatch
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [staging, production]
dry-run:
description: 'Skip actual deployment'
type: boolean
default: falseInput types: string, boolean, choice, environment. Access via ${{ inputs.environment }}.
Webhook Events
on:
release:
types: [published]
issues:
types: [opened, labeled]
workflow_call:
inputs:
ref:
type: string
required: true
secrets:
token:
required: trueworkflow_call makes a workflow reusable by other workflows.
Jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: pnpm lint
test:
runs-on: ubuntu-latest
needs: lint
strategy:
matrix:
node: [20, 22, 24]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- run: pnpm test
deploy:
runs-on: ubuntu-latest
needs: [lint, test]
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v6
- run: ./deploy.shJobs run in parallel by default. Use needs to create dependencies. The if conditional controls whether a job runs.
Runner Selection
| Label | OS | Use Case |
|---|---|---|
ubuntu-latest | Ubuntu LTS | General CI/CD |
ubuntu-24.04 | Ubuntu 24.04 | Pin specific OS version |
windows-latest | Windows | Windows-specific builds |
macos-latest | macOS | iOS/macOS builds |
self-hosted | Custom | Custom hardware or config |
Pin OS versions for reproducibility in production workflows.
Steps
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
env:
NODE_ENV: production
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/Steps use either uses (for actions) or run (for shell commands). The if: always() runs a step regardless of previous step outcomes.
Shell Configuration
defaults:
run:
shell: bash
working-directory: ./app
steps:
- name: PowerShell step
run: Get-Process
shell: pwshAvailable shells: bash, pwsh, python, sh, cmd (Windows only), powershell (Windows only).
Environment Variables
env:
CI: true
NODE_ENV: production
jobs:
build:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
steps:
- name: Print ref
run: echo "Branch is ${{ github.ref_name }}"
env:
STEP_VAR: only-hereVariables cascade: workflow-level, job-level, step-level. Step-level overrides job-level overrides workflow-level.
Setting Outputs Between Steps
steps:
- name: Set version
id: version
run: echo "value=$(cat VERSION)" >> "$GITHUB_OUTPUT"
- name: Use version
run: echo "Version is ${{ steps.version.outputs.value }}"Setting Outputs Between Jobs
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- id: version
run: echo "value=1.2.3" >> "$GITHUB_OUTPUT"
deploy:
needs: prepare
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.prepare.outputs.version }}"Permissions
permissions: {}
jobs:
build:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6Set permissions: {} at workflow level to start with zero permissions, then grant per-job. Available scopes: actions, checks, contents, deployments, id-token, issues, packages, pages, pull-requests, repository-projects, security-events, statuses.
Concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: trueGroups runs by a string key. When cancel-in-progress is true, a new run cancels any in-progress run in the same group. Common pattern for PR workflows to avoid wasting resources on superseded commits.
Job-Level Concurrency
jobs:
deploy:
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false
steps:
- run: ./deploy.shUse cancel-in-progress: false for deployments to avoid partial deploys.
Timeouts
jobs:
test:
timeout-minutes: 30
steps:
- name: Long test
timeout-minutes: 15
run: pnpm test:e2eDefault job timeout is 360 minutes (6 hours). Set explicit timeouts to fail fast on hung processes.
Services (Sidecar Containers)
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- run: pnpm test:integration
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/postgresService containers run alongside the job. Use health checks to wait for readiness. Only available on Linux runners.