CI-CD Updated 2026-07-10 180+ commands Verified against official docs

GitHub CLI and Actions Cheat Sheet

180+ GitHub CLI commands and GitHub Actions workflow patterns. Includes a Failure Decoder for diagnosing broken workflow runs. Verified against official GitHub docs.

Ctrl+K

Everything runs in your browser. No commands or data are sent to any server.

New to GitHub CLI? Start with these

5 essential commands to get you started. The full reference is right below.

Authenticate the GitHub CLI

gh auth login

Authenticate the GitHub CLI with your GitHub account before using any other gh commands.

Create a pull request

gh pr create --fill

Create a pull request from the current branch directly from the terminal without opening the browser.

Merge a pull request

gh pr merge --squash --delete-branch

Merge a pull request from the terminal after it has been approved.

Watch a workflow run live

gh run watch

Watch a GitHub Actions workflow run in real time from the terminal showing live status updates.

Trigger a workflow on push

on: push: branches: - main

Trigger a workflow on every push to the main branch.

180 commands

Authenticate the GitHub CLI

Beginner
gh auth login

↓ Click command to explain

--with-token Reads token from stdin for CI use
--hostname Authenticates to a GitHub Enterprise instance

When to use this

Authenticate the GitHub CLI with your GitHub account before using any other gh commands.

Gotcha

Run gh auth status after logging in to confirm authentication worked correctly.

Create a pull request

Beginner Destructive
gh pr create --fill

↓ Click command to explain

--fill Uses the branch name and commits as the PR title and body
--draft Creates a draft PR
--assignee Assigns the PR
--label Adds labels
--base Sets the target branch

When to use this

Create a pull request from the current branch directly from the terminal without opening the browser.

Gotcha

--fill is the most useful flag for quick PRs as it populates the title and body automatically from your commit messages.

Merge a pull request

Intermediate Destructive
gh pr merge --squash --delete-branch

↓ Click command to explain

--squash Squashes all commits into one
--merge Creates a merge commit
--rebase Rebases commits onto the base branch
--delete-branch Deletes the head branch after merge
--auto Enables auto-merge when checks pass

When to use this

Merge a pull request from the terminal after it has been approved.

Gotcha

--squash --delete-branch is the most common combination for keeping a clean main branch history.

Watch a workflow run live

Beginner
gh run watch

↓ Click command to explain

--exit-status Exits with a non-zero status if the run fails, useful in scripts

When to use this

Watch a GitHub Actions workflow run in real time from the terminal showing live status updates.

Trigger a workflow on push

Beginner
on: push: branches: - main

↓ Click command to explain

When to use this

Trigger a workflow on every push to the main branch.

Gotcha

Always specify branch filters on push triggers or the workflow runs on every push to every branch in the repository which wastes runner minutes.

Check current authentication status

Beginner
gh auth status

↓ Click command to explain

When to use this

Check which GitHub account is currently authenticated and which scopes the token has.

Gotcha

Run this first when any gh command fails with authentication errors.

Log out of a GitHub account

Intermediate Destructive
gh auth logout

↓ Click command to explain

When to use this

Log out of the current GitHub account.

Print the current auth token

Intermediate
gh auth token

↓ Click command to explain

When to use this

Print the current authentication token to stdout for use in scripts or to pass to other tools.

Gotcha

Never log the output of this command in CI as it prints the raw token.

Set a gh CLI configuration value

Intermediate
gh config set editor vim

↓ Click command to explain

--host Sets config for a specific GitHub host

When to use this

Configure gh settings like preferred editor, git protocol, and browser.

Read a gh CLI configuration value

Beginner
gh config get editor

↓ Click command to explain

When to use this

Check the current value of a gh configuration setting.

Add OAuth scopes to an existing token

Intermediate
gh auth refresh --scopes repo,read:org

↓ Click command to explain

When to use this

Add new OAuth scopes to your existing token without re-logging in.

Install a gh CLI extension

Intermediate
gh extension install github/gh-copilot

↓ Click command to explain

When to use this

Install a gh CLI extension to add new commands.

List installed gh CLI extensions

Beginner
gh extension list

↓ Click command to explain

When to use this

List all installed gh CLI extensions.

Upgrade gh CLI extensions

Intermediate
gh extension upgrade --all

↓ Click command to explain

When to use this

Upgrade all installed gh CLI extensions to their latest versions.

Remove a gh CLI extension

Intermediate Destructive
gh extension remove github/gh-copilot

↓ Click command to explain

When to use this

Uninstall a gh CLI extension.

Create and clone a new repository

Beginner Destructive
gh repo create my-project --public --clone

↓ Click command to explain

--public Makes the repo public
--private Makes it private
--internal Makes it internal to an org
--clone Clones the repo locally after creating it
--add-readme Adds a README
--gitignore Adds a .gitignore template
--license Adds a license

When to use this

Create a new GitHub repository and immediately clone it locally in one command.

Gotcha

Without --clone you only create the remote repo and still need to git clone it separately.

Clone a repository by owner/name

Beginner
gh repo clone owner/repo-name

↓ Click command to explain

When to use this

Clone a GitHub repository using the short owner/repo format without needing the full URL.

Fork and clone a repository

Intermediate Destructive
gh repo fork owner/repo --clone

↓ Click command to explain

--clone Clones the fork locally
--remote Adds the original repo as a remote named upstream

When to use this

Fork a repository and clone it locally in one command.

View repository information

Beginner
gh repo view --web

↓ Click command to explain

--web Opens the repo in the browser
--json Outputs specific fields as JSON
--jq Filters JSON output

When to use this

View information about the current repository or open it in the browser.

List repositories

Beginner
gh repo list --limit 30

↓ Click command to explain

--limit Sets maximum number of results
--public Shows only public repos
--private Shows only private repos
--fork Shows only forks
--source Shows only non-forks

When to use this

List repositories for your account or an organization.

Delete a repository

Advanced Destructive
gh repo delete owner/repo --yes

↓ Click command to explain

--yes Skips confirmation prompt

When to use this

Permanently delete a GitHub repository.

Gotcha

Repository deletion is permanent and cannot be undone, all issues PRs and history are lost.

Create an issue

Beginner Destructive
gh issue create --title "Bug: login fails" --body "Steps to reproduce..."

↓ Click command to explain

--title Sets the issue title
--body Sets the issue body
--label Adds labels
--assignee Assigns the issue
--milestone Sets a milestone
--web Opens in browser to finish creating

When to use this

Create a GitHub issue from the terminal.

Gotcha

Use --web if you need rich formatting or file attachments.

List issues

Beginner
gh issue list --label bug --state open

↓ Click command to explain

--label Filters by label
--state Filters by open/closed/all
--assignee Filters by assignee
--author Filters by author
--limit Sets max results

When to use this

List open issues in the current repository filtered by label.

View an issue

Beginner
gh issue view 123

↓ Click command to explain

--web Opens the issue in the browser
--json Outputs as JSON
--comments Shows all comments

When to use this

Read an issue in the terminal without opening the browser.

Close an issue

Intermediate Destructive
gh issue close 123 --comment "Fixed in PR #456"

↓ Click command to explain

--comment Adds a comment when closing
--reason Sets the close reason as completed or not-planned

When to use this

Close an issue from the terminal with an optional comment.

Reopen a closed issue

Intermediate
gh issue reopen 123

↓ Click command to explain

When to use this

Reopen a previously closed issue.

Comment on an issue

Intermediate
gh issue comment 123 --body "Looking into this now"

↓ Click command to explain

--body Sets the comment text
--edit-last Edits your most recent comment
--web Opens in browser

When to use this

Add a comment to an issue from the terminal.

Edit issue metadata

Intermediate
gh issue edit 123 --add-label "priority:high" --remove-label "needs-triage"

↓ Click command to explain

--add-label Adds a label
--remove-label Removes a label
--add-assignee Adds an assignee
--remove-assignee Removes an assignee
--title Changes the title
--milestone Sets a milestone

When to use this

Edit issue metadata from the terminal.

Pin an issue

Intermediate
gh issue pin 123

↓ Click command to explain

When to use this

Pin an issue to the top of the repository issue list.

Search issues

Intermediate
gh search issues "memory leak" --repo owner/repo --state open

↓ Click command to explain

--repo Limits search to a specific repo
--state Filters by open or closed
--label Filters by label
--language Filters by programming language

When to use this

Search for issues across repositories using GitHub search syntax.

Create an issue label

Intermediate Destructive
gh label create "priority:high" --color FF0000 --description "Needs immediate attention"

↓ Click command to explain

When to use this

Create a new issue label in the current repository.

List repository labels

Beginner
gh label list

↓ Click command to explain

When to use this

List all labels in the current repository.

List milestones

Beginner
gh milestone list

↓ Click command to explain

When to use this

List all milestones in the current repository.

Sync a fork with upstream

Intermediate Destructive
gh repo sync owner/fork --source owner/upstream

↓ Click command to explain

When to use this

Sync a fork with its upstream repository.

Open the current repo in a browser

Beginner
gh browse

↓ Click command to explain

--branch Opens a specific branch
--commit Opens a specific commit
--no-browser Prints the URL instead of opening it

When to use this

Open the current repository in the browser from any directory.

List pull requests

Beginner
gh pr list --state open

↓ Click command to explain

--state open/closed/merged/all
--label Filters by label
--assignee Filters by assignee
--author Filters by author
--base Filters by base branch
--draft Shows only drafts

When to use this

List pull requests in the current repository.

View a pull request

Beginner
gh pr view 123

↓ Click command to explain

--web Opens in browser
--json Outputs as JSON
--comments Shows all comments

When to use this

Read a pull request in the terminal.

Check out a pull request branch

Beginner
gh pr checkout 123

↓ Click command to explain

When to use this

Check out the branch of a pull request locally to test it.

Gotcha

This creates a local branch tracking the PR branch so you can run and test the code without manually finding and fetching the branch.

Approve or request changes on a PR

Intermediate
gh pr review 123 --approve --body "LGTM"

↓ Click command to explain

--approve Approves the PR
--request-changes Requests changes
--comment Leaves a comment review
--body Sets the review body text

When to use this

Approve or request changes on a pull request from the terminal.

Comment on a pull request

Intermediate
gh pr comment 123 --body "Please add tests"

↓ Click command to explain

When to use this

Add a comment to a pull request from the terminal.

Edit pull request metadata

Intermediate
gh pr edit 123 --add-reviewer teammate --add-label "needs-review"

↓ Click command to explain

--add-reviewer Adds a reviewer
--remove-reviewer Removes a reviewer
--add-label Adds a label
--title Changes the title
--base Changes the base branch
--draft Converts to draft
--ready-for-review Marks as ready

When to use this

Edit pull request metadata from the terminal.

View a pull request diff

Intermediate
gh pr diff 123

↓ Click command to explain

--color always/never/auto controls color output
--patch Outputs in patch format

When to use this

View the diff of a pull request in the terminal.

See relevant pull request status

Beginner
gh pr status

↓ Click command to explain

When to use this

See the status of pull requests relevant to you, assigned to you, review requested, and your own open PRs.

Mark a draft PR as ready

Intermediate
gh pr ready 123

↓ Click command to explain

When to use this

Convert a draft pull request to ready for review.

Close a pull request

Intermediate Destructive
gh pr close 123 --comment "Closing in favour of #456" --delete-branch

↓ Click command to explain

--comment Adds a closing comment
--delete-branch Deletes the head branch

When to use this

Close a pull request without merging it.

Reopen a closed pull request

Intermediate
gh pr reopen 123

↓ Click command to explain

When to use this

Reopen a closed pull request.

View pull request check status

Beginner
gh pr checks 123

↓ Click command to explain

--watch Watches check status until all checks complete
--interval Sets check poll interval in seconds

When to use this

See the status of all required checks on a pull request.

Enable auto-merge on a pull request

Intermediate Destructive
gh pr merge 123 --auto --squash --delete-branch

↓ Click command to explain

When to use this

Enable auto-merge on a PR so it merges automatically when all required checks pass and approvals are met.

Gotcha

Auto-merge only works if branch protection rules are enabled on the repository.

Search pull requests

Intermediate
gh search prs --assignee @me --state open

↓ Click command to explain

When to use this

Search for pull requests across repositories.

Lock conversation on a pull request

Advanced
gh pr lock 123 --reason resolved

↓ Click command to explain

When to use this

Lock conversation on a pull request to prevent further comments.

Create a draft pull request with explicit title and body

Beginner Destructive
gh pr create --draft --title "WIP: new feature" --body "Work in progress"

↓ Click command to explain

When to use this

Create a draft PR with a specific title and body instead of letting --fill generate them from commits.

Extract specific PR fields as JSON

Intermediate
gh pr view 123 --json state,mergeable,reviews

↓ Click command to explain

When to use this

Get specific pull request fields as structured JSON for use in scripts.

List your own open pull requests

Beginner
gh pr list --author @me

↓ Click command to explain

When to use this

List only pull requests you authored, filtering out everyone else's.

List recent workflow runs

Beginner
gh run list --limit 10

↓ Click command to explain

--workflow Filters by workflow file name
--branch Filters by branch
--status Filters by status completed/in_progress/queued/failure/success
--limit Sets max results

When to use this

List recent workflow runs in the current repository.

View a workflow run's details and logs

Beginner
gh run view 1234567890

↓ Click command to explain

--log Shows full logs
--log-failed Shows only failed step logs
--web Opens in browser
--exit-status Exits non-zero if run failed

When to use this

View the details and logs of a specific workflow run.

Gotcha

gh run view with --log-failed is the fastest way to see only what failed without scrolling through all logs.

Watch a specific workflow run

Beginner
gh run watch 1234567890

↓ Click command to explain

--exit-status Exits with non-zero status if the run fails

When to use this

Stream live status updates of a workflow run until it completes.

Rerun a failed workflow run

Intermediate Destructive
gh run rerun 1234567890 --failed-only

↓ Click command to explain

--failed-only Reruns only the failed jobs
--debug Enables debug logging for the rerun

When to use this

Rerun a failed workflow run without triggering a new push.

Gotcha

--failed-only is much faster than rerunning all jobs when only one job failed.

Cancel a running workflow run

Intermediate Destructive
gh run cancel 1234567890

↓ Click command to explain

When to use this

Cancel a currently running workflow run.

Delete a workflow run

Advanced Destructive
gh run delete 1234567890

↓ Click command to explain

When to use this

Delete a workflow run from the repository history.

List workflow files

Beginner
gh workflow list

↓ Click command to explain

--all Shows disabled workflows too

When to use this

List all workflow files in the current repository.

Manually trigger a workflow

Intermediate Destructive
gh workflow run deploy.yml --field environment=production

↓ Click command to explain

--field Passes an input to a workflow_dispatch trigger
--ref Runs the workflow on a specific branch or tag

When to use this

Manually trigger a workflow that has a workflow_dispatch trigger.

Gotcha

The workflow must have on: workflow_dispatch defined or this command fails.

Re-enable a disabled workflow

Intermediate
gh workflow enable deploy.yml

↓ Click command to explain

When to use this

Re-enable a disabled workflow.

Disable a workflow

Intermediate Destructive
gh workflow disable deploy.yml

↓ Click command to explain

When to use this

Disable a workflow from running.

Set a repository secret

Intermediate Destructive
gh secret set MY_SECRET --body "secret-value"

↓ Click command to explain

--body Passes the value directly
--env Sets a secret at the environment level
--org Sets a secret at the organization level
--repos Limits which repos can access an org secret

When to use this

Set a GitHub Actions secret in the repository.

Gotcha

Secret values are write-only after setting, you cannot read them back through the API or CLI.

List repository secrets

Beginner
gh secret list

↓ Click command to explain

--env Lists secrets for a specific environment
--org Lists org-level secrets

When to use this

List all secrets defined in the repository.

Gotcha

This only lists secret names not values, GitHub never exposes secret values after they are set.

Delete a repository secret

Intermediate Destructive
gh secret delete MY_SECRET

↓ Click command to explain

When to use this

Delete a secret from the repository.

Set a repository variable

Intermediate
gh variable set MY_VAR --body "value"

↓ Click command to explain

When to use this

Set a GitHub Actions variable which unlike secrets is not masked in logs.

Gotcha

Variables are visible in logs unlike secrets, never store sensitive values as variables.

Create a release with assets

Intermediate Destructive
gh release create v1.0.0 --title "Version 1.0.0" --notes "Release notes here" dist/*.zip

↓ Click command to explain

--draft Creates a draft release
--prerelease Marks as pre-release
--notes Sets release notes
--notes-file Reads notes from a file
--generate-notes Auto-generates notes from merged PRs

When to use this

Create a GitHub release and upload release assets in one command.

List releases

Beginner
gh release list

↓ Click command to explain

When to use this

List all releases in the repository.

Minimal workflow structure

Beginner
name: CI on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: npm test

↓ Click command to explain

When to use this

A minimal GitHub Actions workflow that runs on push to main.

Gotcha

Always pin actions to a specific version tag like v4 not just v4 to avoid unexpected breaking changes when the action updates.

Choose a runner environment

Beginner
runs-on: ubuntu-latest

↓ Click command to explain

ubuntu-latest Uses the most recent Ubuntu runner
windows-latest Uses Windows
macos-latest Uses macOS

When to use this

Specify which runner environment to use for a job.

Gotcha

ubuntu-latest is the cheapest runner at 1x the minute multiplier, Windows runners cost 2x and macOS runners cost 10x your billing minutes.

Understand uses versus run

Beginner
steps: - uses: actions/checkout@v4 # runs a pre-built action - run: echo 'hello world' # runs a shell command

↓ Click command to explain

When to use this

Understand the difference between uses which calls a pre-built action and run which executes a shell command.

Gotcha

uses and run cannot both be set on the same step.

Set an environment variable for one step

Beginner
steps: - name: Print var env: MY_VAR: hello run: echo "$MY_VAR"

↓ Click command to explain

When to use this

Pass environment variables to a specific step.

Gotcha

Environment variables set with env at the step level are only available in that step. Use env at the job or workflow level to share variables across steps.

Pass a value between jobs

Intermediate
jobs: build: runs-on: ubuntu-latest outputs: version: ${{ steps.get-version.outputs.version }} steps: - id: get-version run: echo "version=1.0.0" >> $GITHUB_OUTPUT

↓ Click command to explain

When to use this

Pass a value from one job to another job that depends on it.

Gotcha

Use echo key=value >> $GITHUB_OUTPUT to set step outputs. The old syntax of set-output is deprecated and will stop working.

Set and read a step output

Intermediate
steps: - id: my_step run: echo "result=success" >> $GITHUB_OUTPUT - run: echo ${{ steps.my_step.outputs.result }}

↓ Click command to explain

When to use this

Set and read an output from one step in subsequent steps of the same job.

Gotcha

Step ids must use underscores not hyphens if you want to reference them with dot notation in expressions, hyphens silently return empty.

Persist an environment variable across steps

Intermediate
run: echo "MY_VAR=hello" >> $GITHUB_ENV

↓ Click command to explain

When to use this

Set an environment variable that persists for all subsequent steps in the same job.

Gotcha

Variables set with GITHUB_ENV are available in all subsequent steps but not in the current step where they are set.

Set a job timeout

Beginner
jobs: build: runs-on: ubuntu-latest timeout-minutes: 30

↓ Click command to explain

When to use this

Set a maximum runtime for a job to prevent stuck jobs from consuming all your runner minutes.

Gotcha

Without timeout-minutes a job runs for up to 6 hours by default, always set a realistic timeout to limit wasted minutes.

Allow a step to fail without failing the job

Intermediate
steps: - name: Flaky test continue-on-error: true run: npm run flaky-test

↓ Click command to explain

When to use this

Allow a step to fail without failing the entire job.

Gotcha

continue-on-error: true means the step outcome is failure but the job continues. The overall job still reports success which can hide real failures if overused.

Run a step in a specific directory

Beginner
steps: - name: Run in subdirectory working-directory: ./frontend run: npm install

↓ Click command to explain

When to use this

Run a step in a specific directory without manually cd-ing into it.

Name a workflow

Beginner
name: CI Pipeline

↓ Click command to explain

When to use this

Give a workflow a descriptive name that appears in the Actions tab instead of the filename.

Set default shell and directory for all steps

Intermediate
defaults: run: shell: bash working-directory: ./app

↓ Click command to explain

When to use this

Set a default shell and working directory that applies to every run step in a job without repeating it on each step.

Trigger on push with branch filters

Beginner
on: push: branches: - main - 'release/**'

↓ Click command to explain

When to use this

Trigger only on pushes to main or any release branch.

Gotcha

Without branch filters every push to every branch triggers the workflow.

Trigger on pull request events

Beginner
on: pull_request: branches: - main types: - opened - synchronize - reopened

↓ Click command to explain

When to use this

Trigger on pull requests targeting main that are opened updated or reopened.

Gotcha

The default types for pull_request are opened synchronize and reopened so specifying them explicitly is optional but makes intent clear.

Trigger on a cron schedule

Intermediate
on: schedule: - cron: '0 9 * * 1-5'

↓ Click command to explain

When to use this

Trigger a workflow on a schedule using cron syntax.

Gotcha

Scheduled workflows only run on the default branch. Also the cron timezone is UTC so adjust accordingly for your local time.

Add a manual trigger with inputs

Beginner
on: workflow_dispatch: inputs: environment: description: 'Deploy environment' required: true default: 'staging' type: choice options: - staging - production

↓ Click command to explain

When to use this

Add a manual trigger button to a workflow with optional input parameters.

Gotcha

The workflow must be on the default branch or a specific branch to be triggerable from the Actions tab UI.

Make a workflow reusable

Intermediate
on: workflow_call: inputs: environment: required: true type: string secrets: DEPLOY_KEY: required: true

↓ Click command to explain

When to use this

Make a workflow reusable so it can be called from other workflows.

Gotcha

Secrets passed to a called workflow must be explicitly declared in the workflow_call trigger.

Trigger when a release is published

Intermediate
on: release: types: - published

↓ Click command to explain

When to use this

Trigger a workflow when a GitHub release is published.

Gotcha

Use published not created since draft releases trigger created but not published.

Trigger only when specific paths change

Intermediate
on: push: paths: - 'src/**' - '!src/tests/**'

↓ Click command to explain

When to use this

Only trigger a workflow when specific files or directories change.

Gotcha

The exclamation mark prefix excludes paths. If only excluded paths change the workflow does not trigger.

Trigger with secrets access on fork PRs

Advanced
on: pull_request_target: types: [opened, synchronize]

↓ Click command to explain

When to use this

Trigger a workflow with access to secrets on pull requests from forks.

Gotcha

Never checkout and run code from the fork PR in a pull_request_target workflow as it has access to secrets. This is a serious security risk documented in the GitHub security advisory.

Trigger on issue or PR comments

Intermediate
on: issue_comment: types: - created

↓ Click command to explain

When to use this

Trigger a workflow when someone comments on an issue or pull request.

Gotcha

This fires on comments to both issues and pull requests, check github.event.issue.pull_request to determine if the comment is on a PR.

Trigger only on version tags

Intermediate
on: push: tags: - 'v*'

↓ Click command to explain

When to use this

Trigger a workflow only when a version tag is pushed.

Trigger when a branch or tag is deleted

Advanced
on: delete:

↓ Click command to explain

When to use this

Trigger a workflow when a branch or tag is deleted, useful for cleanup automation like removing a matching preview environment.

Trigger when a branch or tag is created

Advanced
on: create:

↓ Click command to explain

When to use this

Trigger a workflow when a branch or tag is created.

Trigger when the repository is forked

Advanced
on: fork:

↓ Click command to explain

When to use this

Trigger a workflow whenever someone forks the repository.

Trigger when someone stars the repository

Advanced
on: watch: types: [started]

↓ Click command to explain

When to use this

Trigger a workflow when someone stars the repository, sometimes used for fun community automations like a thank-you comment bot.

Trigger on GitHub Discussions activity

Advanced
on: discussion: types: [created]

↓ Click command to explain

When to use this

Trigger a workflow when a new GitHub Discussion is created.

Trigger when a check run completes

Advanced
on: check_run: types: [completed]

↓ Click command to explain

When to use this

Trigger a workflow when a check run completes, useful for chaining automation after a third-party check reports its result.

Trigger when a deployment status changes

Advanced
on: deployment_status:

↓ Click command to explain

When to use this

Trigger a workflow when a deployment status changes, such as running smoke tests after a deployment reports success.

Trigger a workflow after another workflow completes

Advanced
on: workflow_run: workflows: ["Build"] types: [completed]

↓ Click command to explain

When to use this

Chain workflows together so one workflow starts automatically after another named workflow finishes.

Trigger on all branches except matching ones

Intermediate
on: push: branches-ignore: - 'docs/**'

↓ Click command to explain

When to use this

Trigger a workflow on pushes to any branch except ones matching a pattern, such as documentation branches.

Gotcha

branches and branches-ignore cannot both be used in the same trigger block.

Skip triggering when only ignored paths change

Intermediate
on: push: paths-ignore: - '**.md'

↓ Click command to explain

When to use this

Skip triggering a workflow when only documentation or other ignored files change.

Get the commit SHA

Intermediate
${{ github.sha }}

↓ Click command to explain

When to use this

Get the full commit SHA that triggered the workflow.

Gotcha

On pull_request events this is the SHA of the auto-generated merge commit not your branch HEAD. Use github.event.pull_request.head.sha to get the actual PR branch commit SHA.

Get the full ref

Intermediate
${{ github.ref }}

↓ Click command to explain

When to use this

Get the full ref that triggered the workflow such as refs/heads/main or refs/tags/v1.0.0.

Gotcha

github.ref includes the refs/heads/ prefix. Use github.ref_name to get just the branch or tag name without the prefix.

Get just the branch or tag name

Beginner
${{ github.ref_name }}

↓ Click command to explain

When to use this

Get just the branch name or tag name without the refs/heads/ or refs/tags/ prefix.

Gotcha

This is almost always what you want instead of github.ref when you need the branch name.

Get source and target branch on a PR

Intermediate
# On pull_request events: # github.head_ref = source branch (your feature branch) # github.base_ref = target branch (usually main)

↓ Click command to explain

When to use this

Get the source and target branch names on pull request events.

Gotcha

github.head_ref and github.base_ref are only populated on pull_request events. On push events they are empty.

Get who triggered the workflow

Beginner
${{ github.actor }}

↓ Click command to explain

When to use this

Get the username of the person or bot that triggered the workflow.

Get the triggering event name

Beginner
${{ github.event_name }}

↓ Click command to explain

When to use this

Get the name of the event that triggered the workflow such as push pull_request or schedule.

Get the unique run ID

Intermediate
${{ github.run_id }}

↓ Click command to explain

When to use this

Get the unique ID of the current workflow run for building artifact names or URLs.

Get the owner and repository name

Beginner
${{ github.repository }}

↓ Click command to explain

When to use this

Get the owner and repository name in the format owner/repo.

Run a step conditionally

Intermediate
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

↓ Click command to explain

When to use this

Run a step or job only when a specific condition is met.

Gotcha

In if conditions you do not use double curly braces. The expression is evaluated directly without dollar sign double-brace wrapping.

Check for a label with contains

Intermediate
if: contains(github.event.pull_request.labels.*.name, 'deploy')

↓ Click command to explain

When to use this

Check if a PR has a specific label before running a deployment step.

Run a step only on failure

Intermediate
if: failure()

↓ Click command to explain

When to use this

Run a step only when a previous step has failed, useful for sending notifications on failure.

Gotcha

if: failure() still requires all previous steps to have completed. Use always() if you want the step to run even if the job is cancelled.

Reference an environment variable in an expression

Beginner
${{ env.MY_VARIABLE }}

↓ Click command to explain

When to use this

Reference an environment variable set at the workflow job or step level inside an expression.

Reference a secret in an expression

Beginner
${{ secrets.MY_SECRET }}

↓ Click command to explain

When to use this

Reference a repository or organization secret inside an expression or step.

Gotcha

Secrets are masked in logs automatically when referenced as ${{ secrets.NAME }} but derived values or manually echoed secrets may not be masked.

Reference a repository variable

Beginner
${{ vars.MY_VARIABLE }}

↓ Click command to explain

When to use this

Reference a repository or organization variable which unlike secrets is visible in logs.

Gotcha

Use vars for non-sensitive configuration values and secrets for anything sensitive.

Access another job's output

Intermediate
${{ needs.build.outputs.version }}

↓ Click command to explain

When to use this

Access output values from a job that the current job depends on.

Gotcha

The needs context is only available in jobs that declare an explicit needs dependency.

Get information about the runner

Intermediate
${{ runner.os }}

↓ Click command to explain

When to use this

Get information about the runner executing the current job such as the operating system.

Access the full event payload

Intermediate
${{ github.event.head_commit.message }}

↓ Click command to explain

When to use this

Access data from the webhook event payload that triggered the workflow, such as the commit message on a push event.

Convert a context to JSON for debugging

Intermediate
${{ toJSON(github.event) }}

↓ Click command to explain

When to use this

Convert a context object to a formatted JSON string, useful for printing the full event payload when debugging a workflow.

Build a formatted string

Intermediate
${{ format('{0}-{1}', github.actor, github.run_id) }}

↓ Click command to explain

When to use this

Build a formatted string from multiple context values, similar to a template string.

Run a step regardless of job status

Intermediate
if: always()

↓ Click command to explain

When to use this

Run a step regardless of whether previous steps failed, useful for cleanup steps that must always execute.

Gotcha

always() still respects a cancelled workflow in some edge cases, pair it with a timeout on the cleanup step itself for full reliability.

Access a repository secret

Beginner
${{ secrets.MY_SECRET }}

↓ Click command to explain

When to use this

Access a repository-level secret in a workflow step.

Gotcha

Secrets set at the repository level are available to all workflows in that repository by default.

Scope secrets to a deployment environment

Intermediate
jobs: deploy: environment: production runs-on: ubuntu-latest

↓ Click command to explain

When to use this

Use environment-level secrets that are only available to jobs that specify that environment, allowing different secrets for staging and production.

Gotcha

Environment secrets require the job to declare environment: environment-name and the environment must be configured in repository settings.

Grant write permissions to GITHUB_TOKEN

Intermediate
permissions: contents: read packages: write pull-requests: write

↓ Click command to explain

When to use this

Grant specific write permissions to the GITHUB_TOKEN for a workflow or job.

Gotcha

Without explicit write permissions the GITHUB_TOKEN only has read access since the May 2021 security update. This is the most common cause of 403 errors in workflows.

Use the automatic GitHub token

Beginner
${{ secrets.GITHUB_TOKEN }}

↓ Click command to explain

When to use this

Use the automatically provided GitHub token to authenticate API calls and push to the repository from within a workflow.

Gotcha

The GITHUB_TOKEN expires when the workflow finishes. It cannot trigger other workflows to prevent infinite loops.

Set a shared organization secret

Intermediate Destructive
gh secret set MY_SECRET --org myorg --body 'value' --repos repo1,repo2

↓ Click command to explain

When to use this

Set a secret at the organization level that can be shared across multiple repositories.

Set workflow-level environment variables

Beginner
env: NODE_ENV: production API_URL: https://api.example.com

↓ Click command to explain

When to use this

Set environment variables at the workflow job or step level.

Gotcha

Use vars context for repository variables set in settings and env key for hardcoded values in the workflow file.

Grant OIDC token permission

Advanced
permissions: id-token: write contents: read

↓ Click command to explain

When to use this

Grant OIDC token permission so a workflow can authenticate to a cloud provider like AWS or Azure without storing long-lived credentials as secrets.

Grant read access to workflow run data

Advanced
permissions: actions: read

↓ Click command to explain

When to use this

Grant read access to workflow run information, needed by workflows that query other runs through the API.

Grant deployment write access

Advanced
permissions: deployments: write

↓ Click command to explain

When to use this

Grant permission to create and update deployments, needed by workflows that mark GitHub deployment status.

Grant issue write access

Intermediate
permissions: issues: write

↓ Click command to explain

When to use this

Grant permission for a workflow to comment on or label issues automatically.

Grant read access to all scopes

Intermediate
permissions: read-all

↓ Click command to explain

When to use this

Grant read access to all permission scopes at once using the shorthand instead of listing each scope individually.

Explicitly grant zero permissions

Advanced
permissions: {}

↓ Click command to explain

When to use this

Explicitly grant zero permissions to the GITHUB_TOKEN, the most restrictive setting for jobs that only need to run scripts with no repository access.

Use a personal access token to trigger other workflows

Advanced
- uses: actions/checkout@v4 with: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

↓ Click command to explain

When to use this

Use a personal access token instead of GITHUB_TOKEN when a workflow needs to push a commit that should trigger other workflows, since GITHUB_TOKEN cannot do that.

Gotcha

Store the personal access token as a secret and rotate it periodically, since unlike GITHUB_TOKEN it does not expire automatically.

Set a secret for Dependabot

Advanced Destructive
gh secret set MY_SECRET --app dependabot

↓ Click command to explain

When to use this

Set a secret specifically scoped for Dependabot to use when it needs to authenticate to a private package registry.

Get structured secret metadata

Intermediate
gh secret list --json name,updatedAt

↓ Click command to explain

When to use this

Get structured secret metadata for auditing which secrets exist and when they were last updated.

Make a job wait for another job

Intermediate
jobs: test: runs-on: ubuntu-latest deploy: needs: test runs-on: ubuntu-latest

↓ Click command to explain

When to use this

Make one job wait for another to complete successfully before starting.

Gotcha

needs accepts a single string or an array of job ids. A job that needs another only starts if all needed jobs succeed.

Run a job across a matrix of variables

Intermediate
strategy: matrix: os: [ubuntu-latest, windows-latest] node: [18, 20, 22]

↓ Click command to explain

When to use this

Run a job multiple times with different combinations of variables such as testing across multiple operating systems and Node.js versions.

Gotcha

Matrix creates a job for every combination so 2 os values times 3 node versions creates 6 parallel jobs.

Add extra variables to specific matrix combinations

Advanced
strategy: matrix: os: [ubuntu-latest, windows-latest] include: - os: windows-latest npm-flags: --legacy-peer-deps

↓ Click command to explain

When to use this

Add extra variables to specific matrix combinations without creating new combinations.

Remove specific combinations from a matrix

Advanced
strategy: matrix: os: [ubuntu-latest, windows-latest] node: [18, 20] exclude: - os: windows-latest node: 18

↓ Click command to explain

When to use this

Remove specific combinations from the matrix.

Let all matrix jobs finish even if one fails

Intermediate
strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest]

↓ Click command to explain

When to use this

Allow all matrix jobs to complete even if one fails, useful when you want to see results across all environments.

Gotcha

fail-fast defaults to true which cancels all remaining matrix jobs when one fails.

Cancel in-progress runs on new push

Intermediate
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true

↓ Click command to explain

When to use this

Cancel any in-progress run of the same workflow on the same branch when a new run starts.

Gotcha

Without concurrency configuration multiple runs of the same workflow can run simultaneously on the same branch leading to race conditions and wasted minutes.

Run a service container alongside a job

Advanced
services: postgres: image: postgres:15 env: POSTGRES_PASSWORD: password ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

↓ Click command to explain

When to use this

Run a service container alongside a job for integration testing with a real database or other service.

Limit how many matrix jobs run at once

Intermediate
strategy: max-parallel: 2 matrix: node: [18, 20, 22]

↓ Click command to explain

When to use this

Limit how many matrix jobs run simultaneously, useful when a shared resource like a database only supports a few concurrent connections.

Run an entire job conditionally

Intermediate
jobs: deploy: if: github.ref == 'refs/heads/main'

↓ Click command to explain

When to use this

Run an entire job only when a condition is met, such as only deploying from the main branch.

Associate a job with a deployment environment

Intermediate
jobs: deploy: environment: name: production url: https://example.com

↓ Click command to explain

When to use this

Associate a job with a deployment environment so it shows up with a link in the repository's Environments UI.

Run a job inside a Docker container

Advanced
jobs: test: runs-on: ubuntu-latest container: node:20

↓ Click command to explain

When to use this

Run a job inside a specific Docker container instead of directly on the runner, ensuring a consistent environment regardless of what the runner image includes.

Build a matrix dynamically from a previous job

Advanced
strategy: matrix: include: ${{ fromJSON(needs.setup.outputs.matrix) }}

↓ Click command to explain

When to use this

Build a matrix dynamically from JSON produced by a previous job, useful when the set of things to test is computed at runtime rather than hardcoded.

Queue runs instead of cancelling them

Intermediate
concurrency: deploy-production

↓ Click command to explain

When to use this

Queue runs instead of cancelling them, ensuring only one deployment happens at a time without discarding any triggered run.

Gotcha

Without cancel-in-progress, concurrency queues runs one after another rather than cancelling the older one.

Wait for multiple jobs to complete

Intermediate
jobs: deploy: needs: [build, test]

↓ Click command to explain

When to use this

Wait for multiple jobs to complete successfully before starting a job that depends on all of them.

Name matrix jobs descriptively

Intermediate
jobs: test: strategy: matrix: node: [18, 20] name: Test (Node ${{ matrix.node }})

↓ Click command to explain

When to use this

Give each matrix job a descriptive name in the Actions UI instead of the default auto-generated name.

Use a matrix variable as the runner label

Intermediate
runs-on: ${{ matrix.os }}

↓ Click command to explain

When to use this

Use a matrix variable directly as the runs-on value so each matrix combination runs on the operating system specified in that combination.

Call a reusable workflow

Intermediate
jobs: deploy: uses: ./.github/workflows/deploy.yml with: environment: production secrets: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

↓ Click command to explain

When to use this

Call a reusable workflow from another workflow passing inputs and secrets.

Gotcha

Secrets are not automatically inherited by called workflows, each secret must be explicitly passed.

Choose between a reusable workflow and a composite action

Intermediate
# Reusable workflow: separate .yml file, can have multiple jobs # Composite action: action.yml in a folder, flat list of steps only

↓ Click command to explain

When to use this

Understand when to use a reusable workflow versus a composite action.

Gotcha

Use reusable workflows when you need multiple jobs or job-level features like services and matrix. Use composite actions when you need a reusable set of steps within a single job.

Define a composite action

Advanced
# In .github/actions/my-action/action.yml: name: My Action description: Does something inputs: param: required: true runs: using: composite steps: - shell: bash run: echo ${{ inputs.param }}

↓ Click command to explain

When to use this

Define a composite action that can be called with uses in any workflow.

Gotcha

Every run step in a composite action must have shell: bash explicitly set as composite actions do not inherit a default shell.

Define outputs for a reusable workflow

Advanced
on: workflow_call: outputs: result: value: ${{ jobs.build.outputs.result }}

↓ Click command to explain

When to use this

Define outputs a reusable workflow returns to its caller so the caller can use values computed inside the called workflow.

Define an output from a composite action

Advanced
outputs: version: value: ${{ steps.get-version.outputs.version }}

↓ Click command to explain

When to use this

Define an output from a composite action so the calling workflow can read a value the action computed.

Set a default value for a composite action input

Intermediate
inputs: environment: required: false default: staging

↓ Click command to explain

When to use this

Set a default value for a composite action input so callers do not need to specify it every time.

Call a reusable workflow with a matrix

Advanced
jobs: call: strategy: matrix: env: [staging, production] uses: ./.github/workflows/deploy.yml with: environment: ${{ matrix.env }}

↓ Click command to explain

When to use this

Call a reusable workflow multiple times using a matrix, such as deploying to staging and production in parallel.

Call a reusable workflow from another repository

Advanced
uses: owner/repo/.github/workflows/deploy.yml@main

↓ Click command to explain

When to use this

Call a reusable workflow that lives in a different repository, useful for sharing standard CI patterns across an organization.

Gotcha

Pin the ref to a tag or commit SHA instead of a branch like main for production use, since a branch can change unexpectedly.

Define an action that runs in a Docker container

Advanced
runs: using: docker image: Dockerfile

↓ Click command to explain

When to use this

Define an action that runs inside a Docker container built from a Dockerfile instead of as composite steps.

Define a JavaScript action entrypoint

Advanced
runs: using: node20 main: index.js

↓ Click command to explain

When to use this

Define a JavaScript action's entrypoint file, the fastest starting action type since it does not require building a Docker image.

Pass all caller secrets to a called workflow

Advanced
secrets: inherit

↓ Click command to explain

When to use this

Pass every secret available to the caller workflow into the called reusable workflow without listing each one individually.

Gotcha

secrets: inherit is convenient but grants the called workflow access to every secret the caller has, which is a security risk in public repositories or when calling third-party workflows.

Call another action from inside a composite action

Advanced
runs: using: composite steps: - uses: actions/setup-node@v4 with: node-version: 20 - shell: bash run: npm install

↓ Click command to explain

When to use this

Call other actions from inside a composite action's steps, combining multiple existing actions into one reusable unit.

Cache dependencies between runs

Intermediate
- uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node-

↓ Click command to explain

When to use this

Cache dependencies between workflow runs to dramatically speed up builds.

Gotcha

The cache key must change when dependencies change. Using hashFiles on your lock file ensures the cache is invalidated when packages are updated.

Upload a build artifact

Beginner
- uses: actions/upload-artifact@v4 with: name: build-output path: dist/ retention-days: 7

↓ Click command to explain

When to use this

Save build outputs or test reports so they can be downloaded from the workflow run or used in a subsequent job.

Download a build artifact

Intermediate
- uses: actions/download-artifact@v4 with: name: build-output path: dist/

↓ Click command to explain

When to use this

Download an artifact uploaded by a previous job in the same workflow run.

Enable verbose debug logging

Advanced
# Set repository secret: ACTIONS_STEP_DEBUG = true ACTIONS_RUNNER_DEBUG = true

↓ Click command to explain

When to use this

Enable verbose debug logging for all steps and the runner in a workflow run.

Gotcha

Debug logs can expose sensitive values, disable after debugging by removing the secrets.

Manually mask a value in logs

Advanced
run: echo "::add-mask::$MY_DERIVED_VALUE"

↓ Click command to explain

When to use this

Manually mask a value in workflow logs so it appears as asterisks even if it was derived from a secret and therefore not automatically masked.

Gotcha

Always mask derived secret values because GitHub only automatically masks the original secret value not values computed from it.

Target a self-hosted runner by label

Intermediate
runs-on: [self-hosted, linux, x64]

↓ Click command to explain

When to use this

Target a self-hosted runner with specific labels for jobs that need custom hardware, software, or network access.

Gotcha

Self-hosted runners must have the GitHub Actions runner application installed and registered with the repository or organization.

Open an SSH session into a failed runner

Advanced
- uses: mxschmitt/action-tmate@v3 if: failure() with: limit-access-to-actor: true

↓ Click command to explain

When to use this

Open an interactive SSH session into the runner when a workflow fails for live debugging.

Gotcha

The tmate session keeps the runner occupied until you disconnect or the timeout is reached which consumes runner minutes.

Use built-in caching in setup-node

Beginner
- uses: actions/setup-node@v4 with: node-version: 20 cache: npm

↓ Click command to explain

When to use this

Use the built-in caching option in setup-node instead of manually configuring actions/cache, which is simpler and covers the common case.

Skip a step when the cache was already restored

Advanced
- id: cache uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ hashFiles('**/package-lock.json') }} - if: steps.cache.outputs.cache-hit != 'true' run: npm ci

↓ Click command to explain

When to use this

Skip an expensive install step entirely when the cache step reports a hit, saving even the time it takes to run a no-op install.

Get a per-job temporary directory

Intermediate
${{ runner.temp }}

↓ Click command to explain

When to use this

Get a temporary directory path that is unique per job and cleaned up automatically, useful for scratch files that should not be cached or committed.

Rerun a workflow with debug logging from the CLI

Advanced Destructive
gh run rerun 1234567890 --debug

↓ Click command to explain

When to use this

Rerun a workflow with debug logging enabled directly from the CLI without setting repository secrets first.

Run GitHub Actions workflows locally

Advanced
act push

↓ Click command to explain

When to use this

Run GitHub Actions workflows locally for fast iteration without pushing commits, using the third-party act tool.

Gotcha

act runs workflows in Docker containers that approximate but do not perfectly match GitHub-hosted runners, so treat it as a fast local check rather than a full replacement for a real run.

Write a custom job summary

Intermediate
run: echo "### Build Summary" >> $GITHUB_STEP_SUMMARY

↓ Click command to explain

When to use this

Write custom markdown to the workflow run summary page, useful for surfacing test results or deployment links without digging through logs.

Get structured run data for scripting

Intermediate
gh run list --json databaseId,status,conclusion --limit 5

↓ Click command to explain

When to use this

Get structured workflow run data as JSON for use in scripts that need to check recent run status programmatically.

Target a self-hosted runner group

Advanced
runs-on: group: my-runner-group

↓ Click command to explain

When to use this

Target a specific runner group instead of individual labels, common in organizations that manage multiple pools of self-hosted runners.

List GitHub Actions caches

Beginner
gh cache list

↓ Click command to explain

When to use this

List all GitHub Actions caches stored for the repository to see what is consuming cache storage.

Reference Tools

Describe what you see and find the real cause. These are the most searched GitHub Actions errors on Stack Overflow with combined views in the millions.

My expression prints empty or blank value

Why this happens

You are mixing GitHub Actions expression syntax with shell variable syntax. Inside a run step dollar double-brace is evaluated first, then the shell runs. Using $name instead of ${{ inputs.name }} or vice versa produces an empty string with no error message.

How to fix it

Use ${{ inputs.name }} for GitHub Actions expressions and $NAME for shell environment variables. Never mix them in the same reference.

Wrong: echo $github.sha Correct: echo ${{ github.sha }} Correct: echo "$MY_ENV_VAR"

github.sha gives wrong commit on pull_request trigger

Why this happens

On pull_request events github.sha is the merge commit SHA that GitHub creates automatically to test the merge, not the HEAD commit of your branch. This causes confusion when you tag Docker images or artifacts with github.sha and the SHA does not match what you see in git log.

How to fix it

Use github.event.pull_request.head.sha to get the actual HEAD SHA of the pull request branch. Use github.sha only on push events where it reliably refers to the commit that was pushed.

# On pull_request use: ${{ github.event.pull_request.head.sha }} # On push use: ${{ github.sha }}

Error 403 when writing to packages, pages, or pull requests

Why this happens

GitHub Actions workflows have read-only permissions by default since the May 2021 security update. The GITHUB_TOKEN can read everything but cannot write to packages, create releases, write to pull requests, or deploy to Pages without explicit permission grants.

How to fix it

Add a permissions block to your job or workflow specifying exactly what write access is needed. The minimum permission principle applies: only grant what the job actually needs.

permissions: contents: write packages: write pull-requests: write

Secret is empty in a called reusable workflow

Why this happens

Secrets are not automatically inherited by called reusable workflows. The caller must explicitly pass secrets using the secrets keyword. Using secrets: inherit passes all secrets but this is a security risk in public repositories.

How to fix it

Explicitly pass each secret the called workflow needs using the secrets block in the workflow_call trigger of the called workflow and the with or secrets block in the caller.

# In caller workflow: jobs: call-workflow: uses: ./.github/workflows/deploy.yml secrets: MY_SECRET: ${{ secrets.MY_SECRET }}

Step output or step id reference returns empty value

Why this happens

GitHub Actions step ids and output names that contain hyphens cannot be referenced with dot notation in expressions. Hyphens in step ids must use bracket notation. Using a hyphenated step id with dot notation silently returns empty string.

How to fix it

Either rename step ids to use underscores instead of hyphens, or use bracket notation to reference outputs from hyphenated step ids.

# Problematic: id: build-app # Then: ${{ steps.build-app.outputs.version }} fails silently # Fix option 1 - use underscore: id: build_app # Then: ${{ steps.build_app.outputs.version }} works # Fix option 2 - bracket notation: ${{ steps["build-app"].outputs.version }}

Composite action run steps fail with no shell specified error

Why this happens

Composite actions do not inherit a default shell from the caller workflow. Every run step inside a composite action must explicitly declare shell: bash or shell: sh. This is different from regular workflow jobs where the default shell is inherited.

How to fix it

Add shell: bash to every run step in your composite action definition.

steps: - name: Run script shell: bash run: echo 'hello'

Workflow cannot access secrets on pull_request from a fork

Why this happens

For security reasons GitHub does not pass secrets to workflows triggered by pull_request events from fork repositories. This prevents malicious PRs from exfiltrating secrets. The actions/checkout@v7 update in June 2026 also changed how fork PR code is fetched by default.

How to fix it

Use the pull_request_target event instead of pull_request for workflows that need secrets. Be extremely careful with pull_request_target since it runs in the context of the base branch and can access secrets, so never checkout and run untrusted code from the fork in a pull_request_target workflow.

on: pull_request_target: types: [opened, synchronize]

Workflow stuck on Waiting for a runner to pick up this job

Why this happens

GitHub-hosted runners have capacity limits and during peak times jobs queue. This is more common with larger runner sizes. The free tier 2000 minutes per month can also cause delays if you are near the limit.

How to fix it

For critical workflows consider self-hosted runners. For non-critical workflows add a timeout-minutes to the job so stuck jobs fail fast rather than consuming billable minutes. Check your organization billing page to verify you have remaining minutes.

jobs: build: runs-on: ubuntu-latest timeout-minutes: 15

Workflow runs duplicate times or old run completes after new run starts

Why this happens

Without concurrency configuration GitHub Actions runs every triggered workflow independently. On fast-push branches this means multiple runs of the same workflow overlap, wasting minutes at best and overwriting a newer deployment with a stale one at worst.

How to fix it

Add a concurrency block with cancel-in-progress: true to cancel older runs when a new one starts for the same branch.

concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true

Secret value appears visible in workflow run logs

Why this happens

GitHub automatically masks secrets in logs when you use ${{ secrets.MY_SECRET }} directly. But if you assign a secret to an environment variable and then echo that variable, GitHub may not mask it. Base64-encoding or transforming a secret also removes the masking.

How to fix it

Never echo secrets directly. If you must log something for debugging use only a partial value or a hash. Use the add-mask workflow command to manually mask a derived value.

# To mask a derived value: - run: echo "::add-mask::$MY_DERIVED_VALUE" - run: echo "$MY_DERIVED_VALUE" # now masked

Frequently Asked Questions

GitHub CLI, installed as the gh command, brings the parts of GitHub you normally reach for in a browser tab, pull requests, issues, releases, and workflow runs, directly into the terminal where the rest of your development work already happens. Creating a pull request with gh pr create --fill takes one command and reuses your commit messages as the title and body, instead of switching to a browser, finding the compare view, and typing everything again. The same applies to checking out someone else's PR to test it locally, watching a deploy workflow run live, or reading issue comments without losing your place in the terminal. Once gh is authenticated, almost everything you would normally click through on github.com has a direct command equivalent.

GitHub Actions changed continuous integration for small teams by removing the setup cost that used to keep CI out of reach for smaller projects. Before Actions, adding CI to a repository usually meant signing up for a separate service, connecting it to GitHub, and maintaining a second system with its own billing and its own configuration format. Actions ships inside the same repository as a YAML file in .github/workflows, triggered automatically by the same push and pull request events developers already generate, with a generous free tier for public repositories. A team can go from no CI at all to running tests on every pull request in the time it takes to commit one workflow file, which is a big part of why Actions became the default choice for new projects rather than an add-on decided later.

The syntax of a GitHub Actions workflow trips up almost everyone the first few times, usually in the same handful of places. Mixing shell variable syntax with GitHub Actions expression syntax, writing $VAR instead of ${{ inputs.var }} or the reverse, silently produces an empty string instead of an error. Step ids that contain a hyphen cannot be referenced with dot notation in an expression, so a step named build-app makes steps.build-app.outputs.version return nothing until the id is changed to use an underscore or the reference switches to bracket notation. Composite actions are another common trap, since every run step inside one must declare its own shell explicitly, unlike a normal job where the shell is inherited automatically. None of these produce a clear error message, which is exactly why they show up so often in troubleshooting threads.

The security model behind GITHUB_TOKEN and secrets is worth understanding before writing workflows that deploy anything or touch sensitive data. Every workflow run gets a GITHUB_TOKEN automatically, scoped to that single run, expiring when the run finishes, and since a security update in May 2021 defaulting to read-only access unless the workflow explicitly requests write permissions. Secrets referenced as ${{ secrets.NAME }} are masked in logs automatically, but a secret copied into a shell variable and echoed, or transformed through base64 or another encoding, may not be masked, which is why manual masking with the add-mask command matters for anything derived from a secret. Pull requests from forks never receive secrets on the pull_request event for the same reason, and switching to pull_request_target to get around that restriction requires real care since that event runs with access to secrets in the context of the base branch.