180+ GitHub CLI commands and GitHub Actions workflow patterns. Includes a Failure Decoder for diagnosing broken workflow runs. Verified against official GitHub docs.
Everything runs in your browser. No commands or data are sent to any server.
5 essential commands to get you started. The full reference is right below.
gh auth login Authenticate the GitHub CLI with your GitHub account before using any other gh commands.
gh pr create --fill Create a pull request from the current branch directly from the terminal without opening the browser.
gh pr merge --squash --delete-branch Merge a pull request from the terminal after it has been approved.
gh run watch Watch a GitHub Actions workflow run in real time from the terminal showing live status updates.
on:
push:
branches:
- main Trigger a workflow on every push to the main branch.
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.
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.
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.
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.
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.
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.
gh auth logout ↓ Click command to explain
When to use this
Log out of the current GitHub account.
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.
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.
gh config get editor ↓ Click command to explain
When to use this
Check the current value of a gh configuration setting.
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.
gh extension install github/gh-copilot ↓ Click command to explain
When to use this
Install a gh CLI extension to add new commands.
gh extension list ↓ Click command to explain
When to use this
List all installed gh CLI extensions.
gh extension upgrade --all ↓ Click command to explain
When to use this
Upgrade all installed gh CLI extensions to their latest versions.
gh extension remove github/gh-copilot ↓ Click command to explain
When to use this
Uninstall a gh CLI extension.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
gh issue reopen 123 ↓ Click command to explain
When to use this
Reopen a previously closed issue.
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.
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.
gh issue pin 123 ↓ Click command to explain
When to use this
Pin an issue to the top of the repository issue list.
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.
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.
gh label list ↓ Click command to explain
When to use this
List all labels in the current repository.
gh milestone list ↓ Click command to explain
When to use this
List all milestones in the current repository.
gh repo sync owner/fork --source owner/upstream ↓ Click command to explain
When to use this
Sync a fork with its upstream repository.
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.
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.
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.
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.
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.
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.
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.
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.
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.
gh pr ready 123 ↓ Click command to explain
When to use this
Convert a draft pull request to ready for review.
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.
gh pr reopen 123 ↓ Click command to explain
When to use this
Reopen a closed pull request.
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.
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.
gh search prs --assignee @me --state open ↓ Click command to explain
When to use this
Search for pull requests across repositories.
gh pr lock 123 --reason resolved ↓ Click command to explain
When to use this
Lock conversation on a pull request to prevent further comments.
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.
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.
gh pr list --author @me ↓ Click command to explain
When to use this
List only pull requests you authored, filtering out everyone else's.
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.
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.
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.
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.
gh run cancel 1234567890 ↓ Click command to explain
When to use this
Cancel a currently running workflow run.
gh run delete 1234567890 ↓ Click command to explain
When to use this
Delete a workflow run from the repository history.
gh workflow list ↓ Click command to explain
--all Shows disabled workflows too When to use this
List all workflow files in the current repository.
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.
gh workflow enable deploy.yml ↓ Click command to explain
When to use this
Re-enable a disabled workflow.
gh workflow disable deploy.yml ↓ Click command to explain
When to use this
Disable a workflow from running.
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.
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.
gh secret delete MY_SECRET ↓ Click command to explain
When to use this
Delete a secret from the repository.
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.
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.
gh release list ↓ Click command to explain
When to use this
List all releases in the repository.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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: 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
on:
push:
tags:
- 'v*' ↓ Click command to explain
When to use this
Trigger a workflow only when a version tag is pushed.
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.
on:
create: ↓ Click command to explain
When to use this
Trigger a workflow when a branch or tag is created.
on:
fork: ↓ Click command to explain
When to use this
Trigger a workflow whenever someone forks the repository.
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.
on:
discussion:
types: [created] ↓ Click command to explain
When to use this
Trigger a workflow when a new GitHub Discussion is created.
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.
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.
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.
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.
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.
${{ 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.
${{ 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.
${{ 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.
# 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.
${{ github.actor }} ↓ Click command to explain
When to use this
Get the username of the person or bot that triggered the workflow.
${{ 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.
${{ 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.
${{ github.repository }} ↓ Click command to explain
When to use this
Get the owner and repository name in the format owner/repo.
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.
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.
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.
${{ 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.
${{ 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.
${{ 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.
${{ 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.
${{ runner.os }} ↓ Click command to explain
When to use this
Get information about the runner executing the current job such as the operating system.
${{ 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.
${{ 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.
${{ 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.
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.
${{ 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.
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.
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.
${{ 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.
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.
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.
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.
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.
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.
permissions:
issues: write ↓ Click command to explain
When to use this
Grant permission for a workflow to comment on or label issues automatically.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
# 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.
# 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
- 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.
- 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.
# 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.
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.
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.
- 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.
- 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.
- 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.
${{ 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.
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.
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.
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.
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.
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.
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.
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 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.