ЁЯПл The SchoolтА║ЁЯУо CI/CDтА║ЁЯзй рдзрдбрд╛ 02 тАФ pipeline рдЪреА рд░рдЪрдирд╛: рдШрдВрдЯрд╛, рдбреЗрд╕реНрдХ, рдХрд╛рд░рдХреВрди
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯзй рдзрдбрд╛ 02 тАФ pipeline рдЪреА рд░рдЪрдирд╛: рдШрдВрдЯрд╛, рдбреЗрд╕реНрдХ, рдХрд╛рд░рдХреВрди

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 02 ┬╖ рдорд╛рдЧреЗ: lesson-01-why-cicd ┬╖ рдкреБрдвреЗ: lesson-03-first-pipeline


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбрд╛ 01, рдЖрдгрд┐ рдХреЛрдгрддреАрд╣реА pipeline file рд╡рд╛рдЪрд╛рдпрд▓рд╛ рд▓рд╛рдЧрдгрд╛рд░реЗ рд╢рдмреНрдж тАФ рдЖрдгрд┐ рдкрд╣рд┐рд▓реА рдЦрд░реА pipeline, рд╡рд░рдкрд╛рд╕реВрди рдЦрд╛рд▓рдкрд░реНрдпрдВрдд рд╡рд╛рдЪрд▓реЗрд▓реА, рдЕрдЬреВрди рди рдЪрд╛рд▓рд╡рддрд╛. рдЦрд▒реНрдпрд╛ files:

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреНрдпрд╛ mailroom ЁЯУо рдордзреНрдпреЗ рдЬрд╛ рдЖрдгрд┐ courier рдЪреА рдПрдХ рдлреЗрд░реА рдкрд╛рд╣рд╛. рдПрдХ рдШрдВрдЯрд╛ рд╡рд╛рдЬрддреЗ ЁЯФФ тАФ рдирд╡реЗ рдкрд╛рдХреАрдЯ рдЖрд▓реЗ. рддреЛ рдореНрд╣рдгрдЬреЗ trigger: push, pull request, timer, рдХрд┐рдВрд╡рд╛ рдХреЛрдгреАрддрд░реА рдмрдЯрдг рджрд╛рдмрдгреЗ.

рдШрдВрдЯрд╛ рдЕрдиреЗрдХ рдбреЗрд╕реНрдХ ЁЯкС рдЕрд╕рд▓реЗрд▓рд╛ рдПрдХ рдорд╛рд░реНрдЧ рд╕реБрд░реВ рдХрд░рддреЗ. рдкреНрд░рддреНрдпреЗрдХ рдбреЗрд╕реНрдХрд╡рд░ рдПрдХ рдХрд╛рд░рдХреВрди рдЫреЛрдЯреНрдпрд╛ рдХрд╛рдорд╛рдВрдЪреА рдпрд╛рджреА рдХреНрд░рдорд╛рдиреЗ рдХрд░рддреЛ: рдкрд╛рдХреАрдЯ рдЙрдШрдбрдгреЗ, рдкреЗрдиреНрд╕рд┐рд▓ рддрд╛рд╕рдгреЗ, рдЙрддреНрддрд░реЗ рддрдкрд╛рд╕рдгреЗ, рдХрд╛рдЧрджрд╛рд╡рд░ рд╢рд┐рдХреНрдХрд╛ рдорд╛рд░рдгреЗ. рдорд╛рд░реНрдЧ рдореНрд╣рдгрдЬреЗ workflow, рдкреНрд░рддреНрдпреЗрдХ рдбреЗрд╕реНрдХ рдПрдХ job, рдкреНрд░рддреНрдпреЗрдХ рдХрд╛рдо рдПрдХ step, рдЖрдгрд┐ рдХрд╛рд░рдХреВрди рдореНрд╣рдгрдЬреЗ runner тАФ рдкреНрд░рддреНрдпрдХреНрд╖ рдХрд╛рдо рдХрд░рдгрд╛рд░реА рдорд╢реАрди.

рдбреЗрд╕реНрдХ рд╡реЗрдЧрд╡реЗрдЧрд│реНрдпрд╛ рдЦреЛрд▓реНрдпрд╛рдВрдордзреНрдпреЗ рдЖрд╣реЗрдд, рдореНрд╣рдгреВрди рддреНрдпрд╛рдВрдЪреНрдпрд╛рдордзреНрдпреЗ рдЬреЗ рдХрд╛рд╣реА рдЬрд╛рдпрдЪреЗ рддреЗ рдкрд╛рдХрд┐рдЯрд╛рдд ЁЯУО рдЬрд╛рддреЗ (artifact). рддрд╛рд╕рд▓реЗрд▓реНрдпрд╛ рдкреЗрдиреНрд╕рд┐рд▓реАрдВрдЪрд╛ рдбреНрд░реЙрд╡рд░ ЁЯЧДя╕П рдлреЗрд▒реНрдпрд╛рдВрдордзреНрдпреЗ рд╡реЗрд│ рд╡рд╛рдЪрд╡рддреЛ (cache). рдХрд╛рд╣реА рдбреЗрд╕реНрдХрдирд╛ рдмреЕрдЬ ЁЯкк рд▓рд╛рдЧрддреЛ (secret). рд╕реВрдЪрдирд╛ рд╢реЗрд╡рдЯреА рд╕рд░рд╛рд╡ рдлрд▓рдХрд╛рд╡рд░ рдХрд┐рдВрд╡рд╛ рдореБрдЦреНрдп рдлрд▓рдХрд╛рд╡рд░ ЁЯУМ рд▓рд╛рдЧрддрд╛рдд (environments), рдЖрдгрд┐ рдореБрдЦреНрдп рдлрд▓рдХрд╛рдЖрдзреА рдкреНрд░рд╛рдЪрд╛рд░реНрдпрд╛рдВрдЪреА рд╕рд╣реА ЁЯЫС рдпреЗрддреЗ (approval). рдЪрд╛рд░ courier рдХрдВрдкрдиреНрдпрд╛ рд╣реАрдЪ mailroom рд╡рд╛рдкрд░рддрд╛рдд тАФ рддреНрдпрд╛рдВрдЪреНрдпрд╛ рдлреЙрд░реНрдорд╡рд░ рд╡реЗрдЧрд│реЗ рд╢рдмреНрдж рдЕрд╕рддрд╛рдд, рдЖрдгрд┐ рдЖрдЬрдЪрд╛ рддреЛрдЪ рдПрдХрдореЗрд╡ рдЕрд╡рдШрдб рднрд╛рдЧ рдЖрд╣реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart TB
    bell["ЁЯФФ trigger<br/>on push and pull_request"]
    wf["ЁЯУо workflow<br/>ci.yml"]
    j1["ЁЯкС job test node 20"]
    j2["ЁЯкС job test node 22"]
    j3["ЁЯкС job test node 24"]
    s["ЁЯУЭ steps in order<br/>checkout тЖТ setup-node тЖТ cache тЖТ prepare тЖТ test тЖТ upload"]
    r["ЁЯзСтАНЁЯТ╝ runner<br/>ubuntu-latest, a fresh machine per job"]
    bell -->|"1 rings"| wf
    wf -->|"2 fans out"| j1
    wf -->|"2 fans out"| j2
    wf -->|"2 fans out"| j3
    j2 -->|"3 one after another"| s
    s -->|"4 executed by"| r

тЭУ рдХрд╛рдп

ЁЯза рдЪрд╛рд░ рдмреЛрд▓реА, рдПрдХрдЪ рд╢рдмреНрджрд╕рдВрдЧреНрд░рд╣

ЁЯУо mailroom рдордзреНрдпреЗ GitHub Actions CircleCI GitLab CI Jenkins
рд╕рдВрдкреВрд░реНрдг рдорд╛рд░реНрдЧ workflow workflow pipeline pipeline
рдПрдХ рдбреЗрд╕реНрдХ (рд╕реНрд╡рддрдГрдЪреА рдорд╢реАрди) job job stage + job stage
рдбреЗрд╕реНрдХрд╡рд░рдЪреЗ рдПрдХ рдХрд╛рдо step step script line step
рдХрд╛рд░рдХреВрди (рдорд╢реАрди) runner executor runner agent

GitLab рдЪреЗ stages рдХреНрд░рдорд╛рдиреЗ рдЪрд╛рд▓рддрд╛рдд, рддреНрдпрд╛рдВрдЪреЗ jobs рд╢реЗрдЬрд╛рд░реА рд╢реЗрдЬрд╛рд░реА; Jenkins рдЪрд╛ stage job рдЪреНрдпрд╛ рдЬрд╡рд│рдЪрд╛ рдЖрд╣реЗ.

ЁЯдФ рдХрд╛

рдЗрдереВрди рдкреБрдвреЗ рдкреНрд░рддреНрдпреЗрдХ run page, error message рдЖрдгрд┐ doc рд╣реЗрдЪ рд╢рдмреНрдж рд╡рд╛рдкрд░рддреЗ, рдЖрдгрд┐ рдЪреБрдХреАрдЪреНрдпрд╛ рдкрд╛рддрд│реАрд╡рд░рдЪреА рдЪреВрдХ рдорд╣рд╛рдЧ рдкрдбрддреЗ: step 3 рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реА file step 4 рдордзреНрдпреЗ рдЕрд╕рддреЗ рдкрдг рдкреБрдврдЪреНрдпрд╛ job рдордзреНрдпреЗ рдирд╕рддреЗ, рдХрд╛рд░рдг рддреЛ job рд╡реЗрдЧрд│реА рдорд╢реАрди рдЖрд╣реЗ. рдорд╢реАрдирд╕рд╛рдареА "job" рдЖрдгрд┐ command рд╕рд╛рдареА "step" рдореНрд╣рдгрд╛, рдХреА рдзрдбреЗ 04тАУ07 рдордзрд▓реНрдпрд╛ рдЧреЛрдВрдзрд│рд╛рдд рдЯрд╛рдХрдгрд╛рд▒реНрдпрд╛ failures рдЪрд╛ рдПрдХ рд╕рдВрдкреВрд░реНрдг рд╡рд░реНрдЧ рдирд╛рд╣реАрд╕рд╛ рд╣реЛрддреЛ. рд╣реЗ рд╢рдмреНрдж рдкреБрдврдЪреНрдпрд╛ mailroom рдордзреНрдпреЗрд╣реА рдЪрд╛рд▓рддрд╛рдд, рддрд┐рдЪрд╛ logo рдХреЛрдгрддрд╛рд╣реА рдЕрд╕реЛ тАФ рдпрд╛ рдХреЛрд░реНрд╕рдЪреЗ рдЪрд╛рд░-tab pipeline page рдЖрдгрд┐ Docker рд╢рд╛рд│реЗрдЪрд╛ рдзрдбрд╛ 12 рдкрд╛рд╣рд╛.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

ci.yml рд╡рд░рдкрд╛рд╕реВрди рд╡рд╛рдЪрд╛. рдЖрдзреА name рдЖрдгрд┐ рдШрдВрдЯрд╛ тАФ рджреЛрди events, filters рдирд╛рд╣реАрдд, рдореНрд╣рдгреВрди рдХреЛрдгрддреАрд╣реА branch рддреА рд╡рд╛рдЬрд╡рддреЗ:

name: тЬЕ ci

on:
  push:
  pull_request:

рд╕рдВрдкреВрд░реНрдг workflow рд╕рд╛рдареА рджреЛрди settings: permissions: run рдЪрд╛ token рд▓рд╣рд╛рди рдХрд░рддреЗ (рдзрдбрд╛ 06); concurrency: рдирд╡реНрдпрд╛ push рд▓рд╛ рдЬреБрдирд╛ run рд░рджреНрдж рдХрд░реВ рджреЗрддреЗ (рдзрдбрд╛ 03):

permissions:
  contents: read            # lesson 06: the job's own token gets the least it needs

concurrency:                # a newer push cancels the older run of the same branch
  group: ci-${{ github.ref }}
  cancel-in-progress: true

рдордЧ jobs: рдПрдХрдЪ, test, рдЬреНрдпрд╛рд▓рд╛ matrix рддреАрди рдбреЗрд╕реНрдХрдордзреНрдпреЗ рдмрджрд▓рддреЗ (рддреАрди рдлреВрдЯрдкрдЯреНрдЯреНрдпрд╛ ЁЯУП, рдзрдбрд╛ 04), рдкреНрд░рддреНрдпреЗрдХ рдирд╡реНрдпрд╛ runner рд╡рд░, рдкреНрд░рддреНрдпреЗрдХ app/ рдордзреНрдпреЗ:

jobs:
  test:
    name: test (node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false      # lesson 04: let all three rulers finish, even if one fails
      matrix:
        node: [20, 22, 24]  # three rulers тАФ the same homework checked three ways
    defaults:
      run:
        working-directory: app

рд╢реЗрд╡рдЯреА steps, рдХреНрд░рдорд╛рдиреЗ. uses: рдкреНрд░рдХрд╛рд╢рд┐рдд action рдЙрд╕рдиреА рдШреЗрддреЗ; run: shell command рдЪрд╛рд▓рд╡рддреЗ. рдкрд╣рд┐рд▓реНрдпрд╛ рджреЛрдиреНрд╣реА uses: рдЖрд╣реЗрдд:

    steps:
      - name: ЁЯУе checkout
        uses: actions/checkout@v4

      - name: ЁЯЯв node ${{ matrix.node }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

рдЖрдгрдЦреА рдЪрд╛рд░ рдпреЗрддрд╛рдд: cache step (id: cache), if: steps.cache.outputs.cache-hit != 'true' рдиреЗ рд░рд╛рдЦрд▓реЗрд▓реА prepare step (рджреЛрдиреНрд╣реА рдзрдбрд╛ 04), ЁЯзк test step (рдзрдбрд╛ 03 рддреА рдЙрджреНрдзреГрдд рдХрд░рддреЛ), рдЖрдгрд┐ if: always() рдЕрд╕рд▓реЗрд▓реА upload step (рдзрдбрд╛ 05).

рд╕реНрд╡рддрдГрдЪреА file рд▓рд┐рд╣рд┐рдгреНрдпрд╛рдЖрдзреА YAML рдЪреЗ рджреЛрди рд╕рд╛рдкрд│реЗ:

  1. YAML 1.1 рдордзреНрдпреЗ on: рд╣рд╛ boolean рдЖрд╣реЗ. GitHub рддреЛ on key рдореНрд╣рдгреВрди рд╡рд╛рдЪрддреЛ, рдкрдг рдХрд╛рд╣реА linters рдЖрдгрд┐ libraries (yamllint рдЪрд╛ truthy рдирд┐рдпрдо, PyYAML, Ruby рдЪреЗ Psych) рддреНрдпрд╛рдЪреЗ true рдХрд░рддрд╛рдд. рдПрдЦрд╛рджреЗ tool рддрдХреНрд░рд╛рд░ рдХрд░рдд рдЕрд╕реЗрд▓, рддрд░ "on": рд▓рд┐рд╣рд╛.
  2. Indentation рдЖрдгрд┐ quoting рдЕрд░реНрде рдард░рд╡рддрд╛рдд. Test step run: > рд╡рд╛рдкрд░рддреЗ, рдПрдХ folded block тАФ рддреАрди рдУрд│реА рдПрдХ command рдмрдирддрд╛рдд; run: | рддреАрди рдЪрд╛рд▓рд╡реЗрд▓. Outputs strings рдЕрд╕рддрд╛рдд, рдореНрд╣рдгреВрди рдЦрд░реА file cache-hit рдЪреА рддреБрд▓рдирд╛ 'true' рд╢реА рдХрд░рддреЗ, quotes рдирд╕рд▓реЗрд▓реНрдпрд╛ true рд╢реА рдирд╛рд╣реА. рдПрдХ рдЬрд╛рдЧрд╛ рдЬрд╛рд╕реНрдд рдбрд╛рд╡реАрдХрдбреЗ рдЕрд╕рд▓реЗрд▓рд╛ list item list рд╕рдВрдкрд╡рддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

# 0) fork and clone (you will need this fork from lesson 03 onward)
gh repo fork BaluRaut/learn-cicd-school --clone && cd learn-cicd-school

# 1) read the file and point at each part of the anatomy
cat .github/workflows/ci.yml
grep -nE '^(on|permissions|concurrency|jobs):|runs-on|matrix:|- name:|uses:|run:' .github/workflows/ci.yml
ruby -ryaml -e 'p YAML.load_file(".github/workflows/ci.yml").keys'   # optional, needs ruby: ["name", true, тАж] тАФ the on: gotcha

# 2) ask GitHub which workflows the fork knows about (enable Actions once in the fork's Actions tab if empty)
gh workflow list

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

тПня╕П рдкреБрдвреЗ

рд╡рд╛рдЪрди рдкреБрд░реЗ. Fork рдХрд░рд╛, push рдХрд░рд╛, рдЖрдгрд┐ рддрдкрд╛рд╕рдгреА рдбреЗрд╕реНрдХрд▓рд╛ тЬЕ рдЪрд╛ рд╢рд┐рдХреНрдХрд╛ рдорд╛рд░рддрд╛рдирд╛ рдкрд╛рд╣рд╛ тАФ рдордЧ рдореБрджреНрджрд╛рдо тЭМ рдЪрд╛ рд╢рд┐рдХреНрдХрд╛ рдорд╛рд░рд╛рдпрд▓рд╛ рд▓рд╛рд╡рд╛, рдореНрд╣рдгрдЬреЗ рд╡реЗрд│ рдпреЗрдгреНрдпрд╛рдЖрдзреАрдЪ рд▓рд╛рд▓ рдХрд╕реЗ рджрд┐рд╕рддреЗ рддреЗ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд│реЗрд▓.

git checkout lesson-03-first-pipeline

ЁЯзй Lesson 02 тАФ Anatomy of a pipeline: the bell, the desks, the clerks

ЁЯУН You are here: Lesson 02 of 12 ┬╖ Previous: lesson-01-why-cicd ┬╖ Next: lesson-03-first-pipeline


ЁЯУж What's in this branch

Lesson 01, plus the vocabulary you need to read any pipeline file тАФ and the first real one, read top to bottom without running it yet. Real files:

ЁЯзТ Explain like I'm 5

Walk into the school mailroom ЁЯУо and watch one round of the courier. A bell rings ЁЯФФ тАФ a new envelope arrived. That is the trigger: a push, a pull request, a timer, or someone pressing a button.

The bell starts a route with several desks ЁЯкС. At each desk a clerk does a short list of tasks in order: open the envelope, sharpen a pencil, check the answers, stamp the sheet. The route is the workflow, each desk a job, each task a step, and the clerk the runner тАФ the machine that actually does the work.

Desks are in different rooms, so anything that must travel between them goes in an envelope ЁЯУО (an artifact). A drawer of sharpened pencils ЁЯЧДя╕П saves time between rounds (a cache). Some desks need a badge ЁЯкк (a secret). Notices end up on the practice board or the main board ЁЯУМ (environments), and the principal's signature ЁЯЫС comes before the main board (an approval). Four courier companies use this same mailroom тАФ with different words on their forms, which is the only hard part of today.

ЁЯЧ║я╕П Diagram

flowchart TB
    bell["ЁЯФФ trigger<br/>on push and pull_request"]
    wf["ЁЯУо workflow<br/>ci.yml"]
    j1["ЁЯкС job test node 20"]
    j2["ЁЯкС job test node 22"]
    j3["ЁЯкС job test node 24"]
    s["ЁЯУЭ steps in order<br/>checkout тЖТ setup-node тЖТ cache тЖТ prepare тЖТ test тЖТ upload"]
    r["ЁЯзСтАНЁЯТ╝ runner<br/>ubuntu-latest, a fresh machine per job"]
    bell -->|"1 rings"| wf
    wf -->|"2 fans out"| j1
    wf -->|"2 fans out"| j2
    wf -->|"2 fans out"| j3
    j2 -->|"3 one after another"| s
    s -->|"4 executed by"| r

тЭУ What

ЁЯза Four dialects, one vocabulary

ЁЯУо in the mailroom GitHub Actions CircleCI GitLab CI Jenkins
the whole route workflow workflow pipeline pipeline
one desk (own machine) job job stage + job stage
one task at a desk step step script line step
the clerk (the machine) runner executor runner agent

GitLab's stages run in sequence, their jobs side by side; Jenkins' stage is closer to a job.

ЁЯдФ Why

Every run page, error message and doc from here on uses these words, and a mistake at the wrong level is expensive: a file written in step 3 exists in step 4 but not in the next job, because that job is a different machine. Say "job" for a machine and "step" for a command, and a whole class of confusing failures in lessons 04тАУ07 goes away. The words also travel to the next mailroom, whatever its logo тАФ see this course's four-tab pipeline page and the Docker school's lesson 12.

ЁЯФз How (in this repo)

Read ci.yml from the top. First the name and the bell тАФ two events, no filters, so any branch rings it:

name: тЬЕ ci

on:
  push:
  pull_request:

Two workflow-wide settings: permissions: shrinks the run's token (lesson 06); concurrency: lets a newer push cancel the older run (lesson 03):

permissions:
  contents: read            # lesson 06: the job's own token gets the least it needs

concurrency:                # a newer push cancels the older run of the same branch
  group: ci-${{ github.ref }}
  cancel-in-progress: true

Then the jobs: one, test, that a matrix turns into three desks (three rulers ЁЯУП, lesson 04), each on a fresh runner, each inside app/:

jobs:
  test:
    name: test (node ${{ matrix.node }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false      # lesson 04: let all three rulers finish, even if one fails
      matrix:
        node: [20, 22, 24]  # three rulers тАФ the same homework checked three ways
    defaults:
      run:
        working-directory: app

Finally the steps, in order. uses: borrows a published action; run: executes a shell command. The first two are both uses::

    steps:
      - name: ЁЯУе checkout
        uses: actions/checkout@v4

      - name: ЁЯЯв node ${{ matrix.node }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

Four more follow: the cache step (id: cache), the prepare step guarded by if: steps.cache.outputs.cache-hit != 'true' (both lesson 04), the ЁЯзк test step (lesson 03 quotes it), and the upload step with if: always() (lesson 05).

Two YAML gotchas before you write your own:

  1. on: is a boolean in YAML 1.1. GitHub reads it as the key on, but some linters and libraries (yamllint's truthy rule, PyYAML, Ruby's Psych) turn it into true. If a tool complains, write "on":.
  2. Indentation and quoting decide meaning. The test step uses run: >, a folded block тАФ three lines become one command; run: | would run three. Outputs are strings, so the real file compares cache-hit to 'true', not a bare true. A list item one space too far left ends the list.

ЁЯзк Try it

# 0) fork and clone (you will need this fork from lesson 03 onward)
gh repo fork BaluRaut/learn-cicd-school --clone && cd learn-cicd-school

# 1) read the file and point at each part of the anatomy
cat .github/workflows/ci.yml
grep -nE '^(on|permissions|concurrency|jobs):|runs-on|matrix:|- name:|uses:|run:' .github/workflows/ci.yml
ruby -ryaml -e 'p YAML.load_file(".github/workflows/ci.yml").keys'   # optional, needs ruby: ["name", true, тАж] тАФ the on: gotcha

# 2) ask GitHub which workflows the fork knows about (enable Actions once in the fork's Actions tab if empty)
gh workflow list

тЪая╕П Common mistakes

тПня╕П Next

Enough reading. Fork, push, and watch the checking desk stamp тЬЕ тАФ then make it stamp тЭМ on purpose, so you know what red looks like before it matters.

git checkout lesson-03-first-pipeline
тЖР Previouswhy cicdNext тЖТfirst pipeline

This page is the lesson's README from the lesson-02-pipeline-anatomy branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.