GitHub Actions Basics: Workflows, Jobs, and Steps¶
Overview¶
Write a working workflow that checks out code, uses an action, sets a variable, evaluates an expression, and prints useful run context — the building blocks of every later module.
Workflow files live in .github/workflows/. Each file defines on: (when it runs), jobs: (what runs where), and under each job a list of steps:. Steps are either run: shell commands or uses: references to actions. Expressions (${{ … }}) and contexts (github, env, vars, secrets) parameterise behaviour without hard-coding every branch name.
This is a core tutorial in Module 2 · GitHub Actions Basics of the REBASH Academy GitHub Actions for Cloud & DevOps Engineers series — written for Cloud, DevOps, Platform, and SRE engineers.
Prerequisites¶
- CI/CD Fundamentals and GitHub Actions
- A GitHub repository you can push to (public is fine for learning)
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Place a workflow under
.github/workflows/and choose events - Structure jobs with
runs-onand ordered steps - Use
actions/checkoutand at least one other official action - Read
github/envcontexts and write a simple expression - Distinguish workflow
env, jobenv, and stepenv
Architecture¶
This topic’s control points and relationships are shown below.
Theory¶
What it is¶
A workflow file is YAML GitHub loads when events fire. Common events for application CI are push, pull_request, and workflow_dispatch (manual). You can filter by branches, paths, and activity types so a docs-only change does not rebuild everything.
A job runs on one runner image (runs-on: ubuntu-latest is the default learning choice). Jobs in the same workflow are independent by default and may run in parallel; you connect them later with needs:. Inside a job, steps share a working directory and environment unless you deliberately isolate them.
An action is a reusable unit published in a repository (or a local ./.github/actions/… composite). Official actions such as actions/checkout and actions/setup-node save you from reinventing clone and toolchain setup. Expressions wrap dynamic values: ${{ github.sha }}, ${{ env.APP_NAME }}, conditionals such as ${{ github.ref == 'refs/heads/main' }}. Variables appear as env: blocks (workflow, job, or step scope) or as repository/organisation configuration variables (vars.*) set in the GitHub UI.
| Piece | You write | Runner sees |
|---|---|---|
on: | Events and filters | Whether the run starts |
jobs.*.runs-on | Label set | Which machine claims the job |
steps[].run | Shell | Exit code fails the step |
steps[].uses | Action ref | Action’s entrypoint |
${{ }} | Expression | Evaluated string/boolean |
Why it matters¶
Almost every production pipeline — lint, test, Docker build, Terraform plan, Kubernetes deploy — is a composition of these primitives. Teams that skip the basics end up with copy-pasted mega-YAML, unclear if: conditions, and secrets leaked via echo. Learning scopes (env vs vars vs secrets), pinning action versions (@v4 or preferably a commit SHA later), and reading the Actions tab logs are core DevOps skills. Pull request status checks also depend on job names staying stable so branch protection rules keep working when you refactor.
How it works¶
- GitHub parses the workflow and matches
on:to the incoming event. - For each eligible job, a runner starts (or is reused from a warm pool on hosted runners).
- Default steps often begin with checkout so the workspace contains your commit.
- Subsequent steps run in order; a failed step fails the job unless
continue-on-erroris set. - Expressions are evaluated by the Actions runtime before the step runs; contexts such as
github,env,vars,secrets,needs, andmatrixsupply data. - Job status rolls up; required checks (configured in branch protection) block merge when red.
For local authoring without burning minutes, write YAML first, validate structure, then push. Tools such as actionlint catch many mistakes before CI.
Key concepts and comparisons¶
| Scope | Keyword | Visibility |
|---|---|---|
| Workflow | top-level env: | All jobs |
| Job | jobs.<id>.env | All steps in that job |
| Step | steps[].env | That step only |
| Repo/org config | vars.NAME | Non-secret settings from UI |
| Secrets | secrets.NAME | Sensitive; never print |
run: | uses: |
|---|---|
| Inline shell you own | Shared action logic |
| Good for one-liners and repo scripts | Good for checkout, setup-*, marketplace tools |
Concurrency note: multiple pushes can start overlapping runs; later modules cover concurrency: groups. For now, know that the newest commit is not always the only run still executing.
Common pitfalls¶
- Putting workflow files outside
.github/workflows/— GitHub will ignore them. - Using
masterin filters when the default branch ismain. - Forgetting
actions/checkoutthen wondering why files are missing. - Printing
${{ secrets.* }}“to debug” — logs may redact, but exfiltration is still possible. - Treating
@v4as immutable forever — tags can move; production often pins SHAs. - Confusing job-level failure with workflow cancellation of sibling jobs (default: other jobs still run).
Hands-on Lab¶
Create a workspace for this tutorial.
mkdir -p ~/rebash-github-actions/module-02/.github/workflows && cd ~/rebash-github-actions/module-02/.github/workflows
Focus: map workflow → jobs → steps with needs between jobs
Step 1 – Write multi-job workflow¶
mkdir -p .github/workflows
cat > .github/workflows/basics.yml << 'EOF'
name: Jobs and steps
on: [push, workflow_dispatch]
permissions:
contents: read
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- id: meta
run: echo "stamp=$(date -u +%Y%m%dT%H%M%SZ)" >> "$GITHUB_OUTPUT"
build:
needs: prepare
runs-on: ubuntu-latest
steps:
- run: mkdir -p out && echo ok > out/marker.txt
- uses: actions/upload-artifact@v4
with:
name: marker
path: out/marker.txt
EOF
```yaml
# Example with outputs (expressions documented raw for mkdocs-macros)
# outputs:
# stamp: ${{ steps.meta.outputs.stamp }}
# build job can read: ${{ needs.prepare.outputs.stamp }}
### Step 2 – Confirm needs and steps
```bash
grep -E 'needs:|steps:|upload-artifact' .github/workflows/basics.yml
test -f .github/workflows/basics.yml
Final step – Cleanup note¶
Validation¶
- Lab commands run under
~/rebash-github-actions/module-02/.github/workflows/ - You can explain each Theory section in your own words
- You used modern tooling where it applies to this topic
- You can describe one production failure mode for this topic
Code Walkthrough¶
Production practice for GitHub Actions Basics: Workflows, Jobs, and Steps always combines:
- Inspect before you change (status, plan, logs, dry-run)
- Prefer reversible, documented changes (Git, IaC, drop-ins, version pins)
- Capture evidence (command output, pipeline logs) for handovers
- Prefer current tools and APIs over legacy shortcuts
- Least privilege — escalate credentials only when required
Keep runbooks short enough to follow under pressure. Automate checks; keep humans for judgement.
Security Considerations¶
- Treat credentials and tokens for github-actions as privileged — never commit them
- Prefer short-lived auth (OIDC, roles, SSO) over long-lived keys
- Validate blast radius before apply/deploy/delete operations
- Restrict who can approve production changes
- Collect audit logs; limit who can read sensitive traces
Common Mistakes¶
Putting workflow files outside .github/workflows/ — GitHub will ignore them.
Validate assumptions against the Theory section and official docs before changing production.
Using master in filters when the default branch is main.
Lab shortcuts (open security groups, admin roles, skip approvals) must not ship unchanged.
Changing production without a rollback path
Always know how to revert (previous artefact, prior release, state rollback, DNS failback).
Best Practices¶
- Encode GitHub Actions Basics: Workflows, Jobs, and Steps changes as code and review them in pull requests
- Pin versions (images, modules, actions, provider plugins)
- Separate environments with clear promotion gates
- Alert on symptoms with runbooks attached
- Destroy lab resources; tag everything with owner and expiry where possible
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Auth / permission denied | Wrong identity, policy, or scope | Check caller identity, roles, and least-privilege policies |
| Timeout / no route | Network, DNS, security group, or endpoint | Trace path, DNS, and allow-lists before retrying |
| Drift / unexpected plan | Manual change or wrong state/workspace | Reconcile desired vs actual; avoid click-ops on managed resources |
| Pipeline/job red | Flaky step, cache, or missing secret | Read failing step logs; bisect recent workflow/config changes |
| Cost spike | Idle load balancer, NAT, oversized compute | Inventory billable resources; stop/delete labs promptly |
Summary¶
GitHub Actions Basics: Workflows, Jobs, and Steps is essential for Cloud and DevOps engineers working with github-actions. Practise the lab until the inspection and change path is muscle memory, then continue the track.
Interview Questions¶
- How do needs and job outputs pass data between jobs?
- Why might a step that works locally fail in Actions?
- What is GITHUB_OUTPUT used for?
- How can overly broad permissions write-all hurt you?
- When should jobs be split versus one large job?
Sample answer — question 2
Confirm the upstream job published outputs, the downstream job declares needs, and expression syntax matches. Also check runner OS path differences.
Sample answer — question 4
Least-privilege tokens limit blast radius if a supply-chain step is compromised.