Quick reference for GitHub Actions CI/CD: workflow syntax, triggers, jobs, matrix strategies, secrets, caching, artifacts, reusable workflows, and essential actions.
GitHub Actions quick-reference covering workflow syntax, triggers, job configuration, expressions, matrix strategies, secrets, caching, artifacts, and reusable workflows. Organized by task for fast lookup when building CI/CD pipelines.
Workflow File Structure
| Key | Description |
|---|
| .github/workflows/*.yml | Workflow files must live in this directory |
| name: | Display name shown in the Actions tab |
| on: | Event triggers that start the workflow |
| permissions: | GITHUB_TOKEN permissions for the workflow |
| env: | Environment variables available to all jobs |
| concurrency: | Prevent concurrent runs (e.g. deploy to same environment) |
| jobs: | Map of job IDs to job configurations |
How Do You Trigger a Workflow?
| Trigger | Description |
|---|
| on: push | Run on every push to any branch |
| on: push: branches: [main] | Run only on pushes to main |
| on: pull_request | Run on PR open, synchronize, reopen |
| on: pull_request: types: [opened, labeled] | Run on specific PR activity types |
| on: schedule: - cron: "0 6 * * 1" | Run on a cron schedule (Mondays at 06:00 UTC) |
| on: workflow_dispatch | Enable manual trigger via the Actions UI |
| on: workflow_dispatch: inputs: | Manual trigger with user-provided inputs |
| on: workflow_call | Make this workflow callable by other workflows |
| on: release: types: [published] | Run when a release is published |
| on: push: paths: ["src/**"] | Run only when files in src/ change |
| on: push: paths-ignore: ["docs/**"] | Skip when only docs/ files change |
Job Configuration
| Key | Description |
|---|
| runs-on: ubuntu-latest | Run on the latest Ubuntu runner |
| runs-on: [self-hosted, linux] | Run on a self-hosted runner with matching labels |
| needs: [build, test] | Run after the listed jobs complete successfully |
| if: success() | Conditional execution (also: failure(), always(), cancelled()) |
| timeout-minutes: 30 | Cancel the job after 30 minutes |
| continue-on-error: true | Allow the workflow to pass even if this job fails |
| environment: production | Link to a deployment environment with protection rules |
| services: | Spin up sidecar containers (e.g. postgres, redis) for the job |
| container: node:20 | Run all steps inside a Docker container |
Step Syntax
| Key | Description |
|---|
| - uses: actions/checkout@v4 | Use a published action (org/repo@ref) |
| - run: npm test | Run a shell command |
| - run: | | Run a multi-line script (YAML block scalar) |
| name: "Run tests" | Display name for the step |
| id: my-step | ID to reference outputs via steps.my-step.outputs.* |
| if: github.ref == 'refs/heads/main' | Conditional step execution |
| with: | Input parameters for the action |
| env: | Environment variables scoped to this step |
| working-directory: ./app | Set the working directory for run commands |
| shell: bash | Specify the shell (bash, pwsh, python, sh) |
Expressions & Contexts
| Expression | Description |
|---|
| ${{ github.sha }} | Full SHA of the commit that triggered the run |
| ${{ github.ref_name }} | Branch or tag name (e.g. main, v1.0.0) |
| ${{ github.event_name }} | Event that triggered the workflow (push, pull_request, etc.) |
| ${{ github.actor }} | Username that triggered the workflow |
| ${{ github.repository }} | Owner/repo (e.g. octocat/hello-world) |
| ${{ secrets.MY_SECRET }} | Access a repository or organization secret |
| ${{ vars.MY_VAR }} | Access a configuration variable |
| ${{ env.MY_VAR }} | Access an environment variable |
| ${{ steps.<id>.outputs.<key> }} | Output from a previous step |
| ${{ needs.<job>.outputs.<key> }} | Output from a dependency job |
| ${{ runner.os }} | Runner OS: Linux, Windows, or macOS |
| ${{ toJSON(github) }} | Convert a context to JSON (useful for debugging) |
Matrix Strategy
| Key | Description |
|---|
| strategy: matrix: | Define variables that create parallel job combinations |
| matrix: { node: [18, 20, 22] } | Run the job once for each value |
| matrix: { os: [ubuntu-latest, windows-latest] } | Test across operating systems |
| fail-fast: false | Continue other matrix jobs even if one fails |
| max-parallel: 2 | Limit concurrent matrix jobs |
| include: [{ node: 22, experimental: true }] | Add extra combinations to the matrix |
| exclude: [{ os: windows-latest, node: 18 }] | Remove specific combinations |
| ${{ matrix.node }} | Reference the current matrix value in steps |
Secrets, Variables & Permissions
| Pattern | Description |
|---|
| ${{ secrets.GITHUB_TOKEN }} | Auto-generated token scoped to the repo |
| ${{ secrets.MY_SECRET }} | Custom secret set in repo or org settings |
| ${{ vars.DEPLOY_ENV }} | Configuration variable (non-sensitive) |
| permissions: contents: read | Limit GITHUB_TOKEN to read-only for contents |
| permissions: pull-requests: write | Allow the workflow to comment on PRs |
| permissions: packages: write | Allow publishing to GitHub Packages |
| permissions: id-token: write | Enable OIDC for cloud provider authentication |
Caching & Artifacts
| Pattern | Description |
|---|
| actions/cache@v4 | Cache dependencies between workflow runs |
| key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | Cache key based on lockfile hash |
| restore-keys: ${{ runner.os }}-node- | Fallback cache key prefix |
| actions/upload-artifact@v4 | Upload build output or test results |
| actions/download-artifact@v4 | Download artifact in a later job |
| retention-days: 5 | Auto-delete artifacts after 5 days |
| actions/setup-node@v4 with: cache: pnpm | Built-in caching for Node.js (npm, yarn, pnpm) |
Reusable Workflows & Composite Actions
| Pattern | Description |
|---|
| on: workflow_call: | Define a reusable workflow that other workflows can call |
| workflow_call: inputs: | Declare input parameters for the reusable workflow |
| workflow_call: secrets: | Declare secrets the caller must pass |
| uses: ./.github/workflows/deploy.yml | Call a reusable workflow in the same repo |
| uses: org/repo/.github/workflows/ci.yml@main | Call a reusable workflow from another repo |
| with: | Pass inputs to the called workflow |
| secrets: inherit | Pass all secrets from caller to called workflow |
| action.yml with runs: composite | Define a composite action bundling multiple steps |
Essential Actions
| Action | Description |
|---|
| actions/checkout@v4 | Check out repository code |
| actions/setup-node@v4 | Install Node.js and optionally cache dependencies |
| actions/setup-python@v5 | Install Python and optionally cache pip/pipenv |
| actions/setup-go@v5 | Install Go and cache modules |
| actions/cache@v4 | Cache files between workflow runs |
| actions/upload-artifact@v4 | Upload artifacts from a job |
| actions/download-artifact@v4 | Download artifacts in a later job |
| actions/github-script@v7 | Run JavaScript with the GitHub API (Octokit) |
| docker/build-push-action@v6 | Build and push Docker images |
| aws-actions/configure-aws-credentials@v4 | Authenticate to AWS via OIDC or access keys |
Frequently Asked Questions
How do you pass data between jobs in GitHub Actions?
Use job outputs. In the producing job, set an output with echo "key=value" >> $GITHUB_OUTPUT in a step with an id, then declare it under jobs.<job>.outputs. The consuming job references it via needs.<job>.outputs.<key>. For files, use upload-artifact and download-artifact.
What is the difference between secrets and variables in GitHub Actions?
Secrets (${{ secrets.X }}) are encrypted, never printed in logs, and meant for tokens, passwords, and keys. Variables (${{ vars.X }}) are plaintext configuration values like environment names or feature flags. Both can be set at the repository, environment, or organization level.
How do you run a job only on the main branch?
Add a conditional: if: github.ref == 'refs/heads/main' at the job level or step level. Alternatively, scope the trigger itself: on: push: branches: [main]. The trigger approach prevents the entire workflow from running, while the if conditional skips only that job.
How do you debug a failing GitHub Actions workflow?
Enable debug logging by setting the repository secret ACTIONS_STEP_DEBUG to true. Use ${{ toJSON(github) }} to dump context. Add run: env to print all environment variables. For interactive debugging, use mxschmitt/action-tmate to SSH into the runner.
What is the difference between uses and run in a step?
uses references a published action (a reusable unit of code from the GitHub Marketplace or another repo), while run executes a shell command directly. You cannot combine both in the same step — each step is either uses or run.
How do reusable workflows differ from composite actions?
Reusable workflows are full workflows called with workflow_call — they run as separate workflow runs and can contain multiple jobs. Composite actions are single actions defined in action.yml that bundle multiple steps into one reusable unit. Use reusable workflows for full CI/CD pipelines and composite actions for shared step sequences.