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

ЁЯЧгя╕П рдзрдбрд╛ 11 тАФ рддреАрдЪ pipeline рдЪрд╛рд░ рдмреЛрд▓реАрдВрдордзреНрдпреЗ: рдЪрд╛рд░ рдХреБрд░рд┐рдЕрд░, рдПрдХ рдорд╛рд░реНрдЧ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-deploy-to-kubernetes ┬╖ рдкреБрдвреАрд▓: lesson-12-pipeline-hygiene


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

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп рд╢рдмреНрджрдХреЛрд╢. рдпрд╛ рдзрдбреНрдпрд╛рдд рдирд╡реАрди рдХрд╛рд╣реАрд╣реА рдШрдбрдд рдирд╛рд╣реА тАФ рддреЛрдЪ рдорд╛рд░реНрдЧ (test тЖТ build & push тЖТ staging тЖТ рд╕рд╣реА тЖТ production) рдЪрд╛рд░ рд╡реЗрд│рд╛ рд▓рд┐рд╣рд┐рд▓реЗрд▓рд╛ рдЖрд╣реЗ, рдЖрдгрд┐ рддреБрдореНрд╣реА рдЪрд╛рд░рд╣реА рд╡рд╛рдЪрд╛рдпрд▓рд╛ рд╢рд┐рдХрддрд╛. рдЦрд▒реНрдпрд╛ files:

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

рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдорд╛рд░реНрдЧрд╛рд╕рд╛рдареА рдЪрд╛рд░ рдХреБрд░рд┐рдЕрд░ рдХрдВрдкрдиреНрдпрд╛ рдмреЛрд▓реА рд▓рд╛рд╡рддрд╛рдд ЁЯУо. рдкреНрд░рддреНрдпреЗрдХ рдЬрдг рддреЛрдЪ рд░рд╕реНрддрд╛ рдЪрд╛рд▓рд╡рддреЗ: рдЧреГрд╣рдкрд╛рда рдШреНрдпрд╛, рддрдкрд╛рд╕рд╛, рдлреЛрдЯреЛрдХреЙрдкреА рдХрд░рд╛, рд▓реЙрдХрд░рдордзреНрдпреЗ рдареЗрд╡рд╛, рд╕рд░рд╛рд╡ рдлрд▓рдХрд╛рд╡рд░ рдкреЛрд╣реЛрдЪрд╡рд╛, рдкреНрд░рд╛рдЪрд╛рд░реНрдпрд╛рдВрдЪреНрдпрд╛ рд╕рд╣реАрдЪреА рд╡рд╛рдЯ рдкрд╛рд╣рд╛, рдореБрдЦреНрдп рдлрд▓рдХрд╛рд╡рд░ рдкреЛрд╣реЛрдЪрд╡рд╛.

рдкрдг рдкреНрд░рддреНрдпреЗрдХ рдХрдВрдкрдиреАрдЪреА рд╕реНрд╡рддрдГрдЪреА рдХрд╛рдЧрджрдкрддреНрд░реЗ ЁЯУД рдЕрд╕рддрд╛рдд. рдПрдХ рдерд╛рдВрдмреНрдпрд╛рд▓рд╛ "job" рдореНрд╣рдгрддреЗ, рджреБрд╕рд░реА "stage". рдПрдХ рдЖрдкрд▓реНрдпрд╛ рдЪрд╛рд▓рдХрд╛рд▓рд╛ "runner" рдореНрд╣рдгрддреЗ, рдПрдХ "agent", рдПрдХ "executor". рддрд┐рдШреА рдЖрдкрд▓реЗ рдлреЙрд░реНрдо YAML рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рддрд╛рдд; рдПрдХ Groovy рдордзреНрдпреЗ. рдорд╛рд░реНрдЧ рдмрджрд▓рдд рдирд╛рд╣реА; рдлреЙрд░реНрдорд╡рд░рдЪреЗ рд╢рдмреНрдж рдмрджрд▓рддрд╛рдд.

рдореНрд╣рдгреВрди рд╣рд╛ рдзрдбрд╛ рдореНрд╣рдгрдЬреЗ рдПрдХ рд╢рдмреНрджрдХреЛрд╢ ЁЯЧгя╕П. рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЪреМрджрд╛ рд╢рдмреНрдж рд╢рд┐рдХрд╛ рдЖрдгрд┐ рдЪрд╛рд░рдкреИрдХреА рдХреЛрдгрддрд╛рд╣реА рдлреЙрд░реНрдо рддреБрдореНрд╣рд╛рд▓рд╛ рдЖрдзреАрдЪ рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛рд╕рд╛рд░рдЦрд╛ рд╡рд╛рдЪрддрд╛ рдпреЗрдИрд▓. рд╕рдЧрд│реНрдпрд╛рдд рдЫрд╛рди рднрд╛рдЧ: рдкреНрд░рддреНрдпрдХреНрд╖ рдХрд╛рдо тАФ node --test, docker build, kubectl rollout status тАФ рдЪрд╛рд░рд╣реА files рдордзреНрдпреЗ рддреАрдЪ shell command рдЖрд╣реЗ. рдмреЛрд▓реА рдореНрд╣рдгрдЬреЗ рдлрдХреНрдд рд╡рд░рдЪреЗ рдЖрд╡рд░рдг.

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

flowchart LR
    route["ЁЯУо one route<br/>test тЖТ build+push тЖТ staging тЖТ ЁЯЫС тЖТ production"]
    gha["тЪЩя╕П GitHub Actions<br/>3 files: ci ┬╖ ship ┬╖ deploy"]
    cci["ЁЯФД CircleCI<br/>.circleci/config.yml"]
    gl["ЁЯжК GitLab CI<br/>.gitlab-ci.yml"]
    jk["ЁЯОй Jenkins<br/>Jenkinsfile"]
    cmds["ЁЯРЪ the same shell lines<br/>node --test ┬╖ docker build ┬╖ kubectl rollout status"]
    route -->|"1"| gha
    route -->|"1"| cci
    route -->|"1"| gl
    route -->|"1"| jk
    gha -->|"2 runs"| cmds
    cci -->|"2 runs"| cmds
    gl -->|"2 runs"| cmds
    jk -->|"2 runs"| cmds

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

ЁЯза Rosetta stone тАФ рдЪрд╛рд░ рдЦрд▒реНрдпрд╛ files рдЬреНрдпрд╛ рдкреНрд░рдХрд╛рд░реЗ рд╢рдмреНрдж рд▓рд┐рд╣рд┐рддрд╛рдд

рд╕рдВрдХрд▓реНрдкрдирд╛ тЪЩя╕П GitHub Actions ЁЯФД CircleCI ЁЯжК GitLab CI ЁЯОй Jenkins
trigger on: push: / pull_request: / workflow_call: / workflow_dispatch: рдЕрдкреНрд░рддреНрдпрдХреНрд╖: push рдХреЗрд▓рд╛ рдХреА workflows: рдЪрд╛рд▓рддрд╛рдд рдЕрдкреНрд░рддреНрдпрдХреНрд╖: push рдХреЗрд▓рд╛ рдХреА pipeline рдЪрд╛рд▓рддреЗ file рдордзреНрдпреЗ рдирд╛рд╣реА, job рд╡рд░ рдард░рд╡рд▓реЗрд▓реЗ (webhook / multibranch scan)
workflow / pipeline рдПрдХ file = рдПрдХ workflow, name: тЬЕ ci workflows: test-build-deploy: рд╕рдВрдкреВрд░реНрдг file; stages: [test, build, deploy] pipeline { тАж }
job / stage jobs: test: + needs: jobs: test: + requires: stage: test рд╕рд╣ test: + needs: stage('тЬЕ test')
step steps: - uses: / - run: steps: - checkout / - run: script: рдУрд│реА steps { sh 'тАж' }
runner runs-on: ubuntu-latest docker: - image: cimg/node:тАж (executor) runner рд╡рд░ image: node:${NODE}-alpine agent { docker { image "node:${NODE}-alpine" } } / agent any
matrix strategy: matrix: node: [20, 22, 24] matrix: parameters: node: [тАж] parallel: matrix: - NODE: [тАж] matrix { axes { axis { name 'NODE' тАж } } }
cache actions/cache@v4, key: prepared-${{ hashFiles('app/prepare.js') }} restore_cache / save_cache, {{ checksum "prepare.js" }} cache: key: files: [app/prepare.js] рдЕрдВрдЧрднреВрдд рдирд╛рд╣реА тАФ persistent workspace, рдХрд┐рдВрд╡рд╛ Job Cacher plugin
artifact actions/upload-artifact@v4 / download-artifact@v4 store_artifacts artifacts: paths: + expire_in рдпрд╛ file рдордзреНрдпреЗ рд╡рд╛рдкрд░рд▓реЗрд▓реЗ рдирд╛рд╣реА (archiveArtifacts рд╣реА step рдЖрд╣реЗ)
test report artifact рдореНрд╣рдгреВрди upload рдХреЗрд▓реЗрд▓реА JUnit file store_test_results artifacts: reports: junit: post { always { junit 'тАж' } }
secret / credential ${{ secrets.X }} / ${{ vars.X }} context: school-cicd project CI/CD variables, $AWS_ROLE_ARN withAWS(credentials: 'aws-school-cicd', тАж)
OIDC badge permissions: id-token: write + configure-aws-credentials role-to-assume: aws-cli/setup: role_arn: id_tokens: + aws sts assume-role-with-web-identity рдпрд╛ file рдордзреНрдпреЗ рдирд╛рд╣реА тАФ рддреНрдпрд╛рдРрд╡рдЬреА credential binding
approval gate environment: production + required reviewers hold-for-approval: type: approval when: manual input message: 'Deploy to production?'
environment environment: staging рдПрдХ job name: + рдПрдХ namespace: parameter environment: { name: staging } deployTo('staging') тАФ function argument
рдлрдХреНрдд main рд╡рд░ on: push: branches: [main] filters: branches: only: main rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when { branch 'main' }

ЁЯдФ рдХрд╛

Teams рднрд╛рд╖рд╛ рдмрджрд▓рддрд╛рдд рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рд╡реЗрд│рд╛ CI vendor рдмрджрд▓рддрд╛рдд, рдЖрдгрд┐ рдиреЛрдХрд░реАрдЪреНрдпрд╛ рдЬрд╛рд╣рд┐рд░рд╛рддреАрдВрдордзреНрдпреЗ рдЪрд╛рд░рд╣реА рдирд╛рд╡реЗ рдЕрд╕рддрд╛рдд. рддреБрдордЪреНрдпрд╛ pipeline рдЪреЗ logic YAML рдордзреНрдпреЗ рдЕрд╕реЗрд▓ рддрд░ migration рдореНрд╣рдгрдЬреЗ рдкреВрд░реНрдг рдкреБрдирд░реНрд▓реЗрдЦрди; рддреЗ scripts рдЖрдгрд┐ package.json рдордзреНрдпреЗ рдЕрд╕реЗрд▓ рддрд░ migration рдореНрд╣рдгрдЬреЗ рдкрд╛рддрд│ рдЖрд╡рд░рдгрд╛рдЪреЗ рднрд╛рд╖рд╛рдВрддрд░ тАФ рдЬреЗ рдиреЗрдордХреЗ рд╡рд░рдЪреНрдпрд╛ рддрдХреНрддреНрдпрд╛рдд рдЖрд╣реЗ. рдпрд╛ рдХреЛрд░реНрд╕рдЪреЗ рдЪрд╛рд░ tab рдЕрд╕рд▓реЗрд▓реЗ рдкрд╛рди рдкреВрд░реНрдг files рд╢реЗрдЬрд╛рд░реА рд╢реЗрдЬрд╛рд░реА рджрд╛рдЦрд╡рддреЗ.

рдкреНрд░рддреНрдпреЗрдХ рдХреБрдареЗ рдЪрдордХрддреЛ (рд╕рд╛рд╡рдзрдкрдгреЗ тАФ рдЪрд╛рд░рд╣реА рдмрджрд▓рдд рд░рд╛рд╣рддрд╛рдд)

рдкреНрд░рддреНрдпреЗрдХ vendor рдЪреА рдореЛрдлрдд рдорд░реНрдпрд╛рджрд╛ рдмрджрд▓рдд рдЕрд╕рддреЗ; рд╣реЗ README рдирд╡реНрд╣реЗ, рд╕рдзреНрдпрд╛рдЪреЗ рдкрд╛рди рддрдкрд╛рд╕рд╛.

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

рдЪрд╛рд░рд╣реА рдЬрд┐рдЪреНрдпрд╛рд╡рд░ рд╕рд╣рдордд рдЖрд╣реЗрдд рддреА рдПрдХ рдУрд│ тАФ рддреАрдЪ test command, рдЪрд╛рд░ рдЖрд╡рд░рдгреЗ:

# .github/workflows/ci.yml
      - name: ЁЯзк test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml
# .circleci/config.yml
      - run:
          name: ЁЯзк test
          command: |
            mkdir -p test-results
            node --test --test-reporter=spec --test-reporter-destination=stdout \
                        --test-reporter=junit --test-reporter-destination=test-results/junit.xml
# .gitlab-ci.yml
  script:
    - cd app
    - node prepare.js                         # prints "already exists" on a cache hit
    - node --test --test-reporter=spec --test-reporter-destination=stdout
                  --test-reporter=junit --test-reporter-destination=test-results.xml
// Jenkinsfile
                sh '''node --test --test-reporter=spec  --test-reporter-destination=stdout \
                                  --test-reporter=junit --test-reporter-destination=test-results.xml'''

Migration рдЪреА рдпреБрдХреНрддреА. app/package.json рдордзреНрдпреЗ рдЖрдзреАрдЪ "test:ci": "node --test --test-reporter=spec тАж --test-reporter=junit тАж" рдЖрд╣реЗ. рдЪрд╛рд░рд╣реА files рдордзреВрди npm run test:ci рдмреЛрд▓рд╛рд╡рд╛, рдореНрд╣рдгрдЬреЗ reporter flags рдПрдХрд╛рдЪ рдЬрд╛рдЧреА рд░рд╛рд╣рддрд╛рдд. Build-and-push shell рдЖрдгрд┐ deploy shell рд╕рд╛рдареАрд╣реА рддреЗрдЪ: рддреЗ scripts/ рдордзреНрдпреЗ рдареЗрд╡рд╛, рдореНрд╣рдгрдЬреЗ рдкреНрд░рддреНрдпреЗрдХ рдмреЛрд▓реА "рдпрд╛ credentials рд╕рд╣ рд╣реА script рдЪрд╛рд▓рд╡рд╛" рдПрд╡рдвреАрдЪ рдЙрд░рддреЗ.

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

# 1) the three load-bearing commands, pulled out of all four dialects
for f in .github/workflows/ci.yml .github/workflows/ship.yml .github/workflows/deploy.yml \
         .circleci/config.yml .gitlab-ci.yml Jenkinsfile; do
  echo "тФАтФА $f"; grep -nE "node --test|docker build|rollout status" "$f"
done

# 2) diff the deploy recipe between two dialects тАФ only the variable spelling differs
recipe() { grep -hE "sed -i|kubectl apply|rollout status" "$1" | sed -E 's/^[ -]*//'; }
diff <(recipe .circleci/config.yml) <(recipe .gitlab-ci.yml)
diff <(recipe .gitlab-ci.yml) <(recipe Jenkinsfile)

# 3) count: how many dialects run each command? (GitHub's three files count as one dialect)
for c in "node --test" "docker build --build-arg APP_VERSION" "rollout status"; do
  n=0
  cat .github/workflows/*.yml | grep -qE "$c" && n=$((n+1))
  for f in .circleci/config.yml .gitlab-ci.yml Jenkinsfile; do grep -qE "$c" "$f" && n=$((n+1)); done
  echo "$c тЖТ $n of 4 dialects"
done

# 4) now the wrapper words: the matrix, four ways
grep -nE "matrix|axes|node: \[|NODE: \[" .github/workflows/ci.yml .circleci/config.yml .gitlab-ci.yml Jenkinsfile

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

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

рддреБрдореНрд╣реА рдЪрд╛рд░рд╣реА рдлреЙрд░реНрдо рд╡рд╛рдЪреВ рд╢рдХрддрд╛. рд╢реЗрд╡рдЯрдЪрд╛ рдзрдбрд╛ рдореНрд╣рдгрдЬреЗ mailroom рдЪреЗ рдирд┐рдпрдордкреБрд╕реНрддрдХ poster: pipeline рд╡рд░реНрд╖рд╛рдиреБрд╡рд░реНрд╖реЗ рд╡рд┐рд╢реНрд╡рд╛рд╕рд╛рд░реНрд╣ рдХрд╢рд╛рдиреЗ рд░рд╛рд╣рддреЗ, рдЖрдгрд┐ ArgoCD рд╢рд╛рд│реЗрдХрдбреЗ рд╣рд╕реНрддрд╛рдВрддрд░.

git checkout lesson-12-pipeline-hygiene

ЁЯЧгя╕П Lesson 11 тАФ The same pipeline in four dialects: four couriers, one route

ЁЯУН You are here: Lesson 11 of 12 ┬╖ Previous: lesson-10-deploy-to-kubernetes ┬╖ Next: lesson-12-pipeline-hygiene


ЁЯУж What's in this branch

Lessons 01тАУ10, plus the phrasebook. Nothing new happens in this lesson тАФ the same route (test тЖТ build & push тЖТ staging тЖТ signature тЖТ production) is written four times, and you learn to read all four. Real files:

ЁЯзТ Explain like I'm 5

Four courier companies bid for the school's route ЁЯУо. Each one drives the same road: pick up the homework, check it, photocopy it, file it in the locker, deliver to the practice board, wait for the principal's signature, deliver to the main board.

But each company has its own paperwork ЁЯУД. One calls a stop a "job", one a "stage". One calls its driver a "runner", one an "agent", one an "executor". Three write their forms in YAML; one writes them in Groovy. The route does not change; the words on the form do.

So this lesson is a phrasebook ЁЯЧгя╕П. Learn the fourteen words that matter and any of the four forms reads like the one you already know. The best part: the actual work тАФ node --test, docker build, kubectl rollout status тАФ is the same shell command in all four files. The dialect is only the wrapper.

ЁЯЧ║я╕П Diagram

flowchart LR
    route["ЁЯУо one route<br/>test тЖТ build+push тЖТ staging тЖТ ЁЯЫС тЖТ production"]
    gha["тЪЩя╕П GitHub Actions<br/>3 files: ci ┬╖ ship ┬╖ deploy"]
    cci["ЁЯФД CircleCI<br/>.circleci/config.yml"]
    gl["ЁЯжК GitLab CI<br/>.gitlab-ci.yml"]
    jk["ЁЯОй Jenkins<br/>Jenkinsfile"]
    cmds["ЁЯРЪ the same shell lines<br/>node --test ┬╖ docker build ┬╖ kubectl rollout status"]
    route -->|"1"| gha
    route -->|"1"| cci
    route -->|"1"| gl
    route -->|"1"| jk
    gha -->|"2 runs"| cmds
    cci -->|"2 runs"| cmds
    gl -->|"2 runs"| cmds
    jk -->|"2 runs"| cmds

тЭУ What

ЁЯза The Rosetta stone тАФ the words as the four real files spell them

concept тЪЩя╕П GitHub Actions ЁЯФД CircleCI ЁЯжК GitLab CI ЁЯОй Jenkins
trigger on: push: / pull_request: / workflow_call: / workflow_dispatch: implicit: a push runs workflows: implicit: a push runs the pipeline set on the job (webhook / multibranch scan), not in the file
workflow / pipeline one file = one workflow, name: тЬЕ ci workflows: test-build-deploy: the whole file; stages: [test, build, deploy] pipeline { тАж }
job / stage jobs: test: + needs: jobs: test: + requires: test: with stage: test + needs: stage('тЬЕ test')
step steps: - uses: / - run: steps: - checkout / - run: script: lines steps { sh 'тАж' }
runner runs-on: ubuntu-latest docker: - image: cimg/node:тАж (executor) image: node:${NODE}-alpine on a runner agent { docker { image "node:${NODE}-alpine" } } / agent any
matrix strategy: matrix: node: [20, 22, 24] matrix: parameters: node: [тАж] parallel: matrix: - NODE: [тАж] matrix { axes { axis { name 'NODE' тАж } } }
cache actions/cache@v4, key: prepared-${{ hashFiles('app/prepare.js') }} restore_cache / save_cache, {{ checksum "prepare.js" }} cache: key: files: [app/prepare.js] none built in тАФ persistent workspace, or the Job Cacher plugin
artifact actions/upload-artifact@v4 / download-artifact@v4 store_artifacts artifacts: paths: + expire_in not used in this file (archiveArtifacts is the step)
test report the JUnit file uploaded as an artifact store_test_results artifacts: reports: junit: post { always { junit 'тАж' } }
secret / credential ${{ secrets.X }} / ${{ vars.X }} context: school-cicd project CI/CD variables, $AWS_ROLE_ARN withAWS(credentials: 'aws-school-cicd', тАж)
OIDC badge permissions: id-token: write + configure-aws-credentials role-to-assume: aws-cli/setup: role_arn: id_tokens: + aws sts assume-role-with-web-identity not in this file тАФ a credential binding stands in
approval gate environment: production + required reviewers hold-for-approval: type: approval when: manual input message: 'Deploy to production?'
environment environment: staging a job name: + a namespace: parameter environment: { name: staging } deployTo('staging') тАФ a function argument
only on main on: push: branches: [main] filters: branches: only: main rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when { branch 'main' }

ЁЯдФ Why

Teams change CI vendors more often than they change languages, and job ads name all four. If your pipeline's logic lives in the YAML, a migration is a rewrite; if it lives in scripts and package.json, the migration is a translation of the thin wrapper тАФ which is exactly the table above. This course's four-tab page shows the complete files side by side.

Where each shines (hedged тАФ all four keep changing)

Each vendor has a free allowance that changes; check the current page, not this README.

ЁЯФз How (in this repo)

The one line all four agree on тАФ the same test command, four wrappers:

# .github/workflows/ci.yml
      - name: ЁЯзк test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml
# .circleci/config.yml
      - run:
          name: ЁЯзк test
          command: |
            mkdir -p test-results
            node --test --test-reporter=spec --test-reporter-destination=stdout \
                        --test-reporter=junit --test-reporter-destination=test-results/junit.xml
# .gitlab-ci.yml
  script:
    - cd app
    - node prepare.js                         # prints "already exists" on a cache hit
    - node --test --test-reporter=spec --test-reporter-destination=stdout
                  --test-reporter=junit --test-reporter-destination=test-results.xml
// Jenkinsfile
                sh '''node --test --test-reporter=spec  --test-reporter-destination=stdout \
                                  --test-reporter=junit --test-reporter-destination=test-results.xml'''

The migration tip. app/package.json already has "test:ci": "node --test --test-reporter=spec тАж --test-reporter=junit тАж". Call npm run test:ci from all four files and the reporter flags live in one place. The same goes for the build-and-push shell and the deploy shell: put them in scripts/ and each dialect shrinks to "run this script with these credentials".

ЁЯзк Try it

# 1) the three load-bearing commands, pulled out of all four dialects
for f in .github/workflows/ci.yml .github/workflows/ship.yml .github/workflows/deploy.yml \
         .circleci/config.yml .gitlab-ci.yml Jenkinsfile; do
  echo "тФАтФА $f"; grep -nE "node --test|docker build|rollout status" "$f"
done

# 2) diff the deploy recipe between two dialects тАФ only the variable spelling differs
recipe() { grep -hE "sed -i|kubectl apply|rollout status" "$1" | sed -E 's/^[ -]*//'; }
diff <(recipe .circleci/config.yml) <(recipe .gitlab-ci.yml)
diff <(recipe .gitlab-ci.yml) <(recipe Jenkinsfile)

# 3) count: how many dialects run each command? (GitHub's three files count as one dialect)
for c in "node --test" "docker build --build-arg APP_VERSION" "rollout status"; do
  n=0
  cat .github/workflows/*.yml | grep -qE "$c" && n=$((n+1))
  for f in .circleci/config.yml .gitlab-ci.yml Jenkinsfile; do grep -qE "$c" "$f" && n=$((n+1)); done
  echo "$c тЖТ $n of 4 dialects"
done

# 4) now the wrapper words: the matrix, four ways
grep -nE "matrix|axes|node: \[|NODE: \[" .github/workflows/ci.yml .circleci/config.yml .gitlab-ci.yml Jenkinsfile

тЪая╕П Common mistakes

тПня╕П Next

You can read all four forms. The last lesson is the mailroom's rulebook poster: what keeps a pipeline trustworthy for years, and the handoff to the ArgoCD school.

git checkout lesson-12-pipeline-hygiene
тЖР Previousdeploy to kubernetesNext тЖТpipeline hygiene

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