ЁЯЧгя╕П рдзрдбрд╛ 11 тАФ рддреАрдЪ pipeline рдЪрд╛рд░ рдмреЛрд▓реАрдВрдордзреНрдпреЗ: рдЪрд╛рд░ рдХреБрд░рд┐рдЕрд░, рдПрдХ рдорд╛рд░реНрдЧ
ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-deploy-to-kubernetes ┬╖ рдкреБрдвреАрд▓: lesson-12-pipeline-hygiene
ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ
рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рддреНрдпрд╛рд╢рд┐рд╡рд╛рдп рд╢рдмреНрджрдХреЛрд╢. рдпрд╛ рдзрдбреНрдпрд╛рдд рдирд╡реАрди рдХрд╛рд╣реАрд╣реА рдШрдбрдд рдирд╛рд╣реА тАФ рддреЛрдЪ рдорд╛рд░реНрдЧ (test тЖТ build & push тЖТ staging тЖТ рд╕рд╣реА тЖТ production) рдЪрд╛рд░ рд╡реЗрд│рд╛ рд▓рд┐рд╣рд┐рд▓реЗрд▓рд╛ рдЖрд╣реЗ, рдЖрдгрд┐ рддреБрдореНрд╣реА рдЪрд╛рд░рд╣реА рд╡рд╛рдЪрд╛рдпрд▓рд╛ рд╢рд┐рдХрддрд╛. рдЦрд▒реНрдпрд╛ files:
- .github/workflows/ci.yml ┬╖ ship.yml ┬╖ deploy.yml тАФ GitHub Actions рдорд╛рд░реНрдЧрд╛рдЪреЗ рддреАрди files рдордзреНрдпреЗ рд╡рд┐рднрд╛рдЬрди рдХрд░рддреЗ
- .circleci/config.yml тАФ CircleCI: рдПрдХ file,
workflows:рдордзреНрдпреЗ рдПрдХрддреНрд░ рдЬреЛрдбрд▓реЗрд▓реЗjobs: - .gitlab-ci.yml тАФ GitLab CI: рдПрдХ file,
stages:рдЖрдгрд┐ YAML anchors - Jenkinsfile тАФ Jenkins declarative pipeline, YAML рдРрд╡рдЬреА Groovy рдордзреНрдпреЗ
- app/package.json тАФ
test:ciрдордзреНрдпреЗ logic рд░рд╛рд╣реВ рд╢рдХрддреЗ, рдореНрд╣рдгрдЬреЗ YAML рдкрд╛рддрд│ рд░рд╛рд╣рддреЗ
ЁЯзТ 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' } |
- Hosted runner = vendor рдЪреЗ рдорд╢реАрди, рдкреНрд░рддреНрдпреЗрдХ job рд╕рд╛рдареА рдирд╡реЗ, рдПрдХрд╛ рдард░рд▓реЗрд▓реНрдпрд╛ рдореЛрдлрдд рдорд░реНрдпрд╛рджреЗрддреВрди рдорд┐рд│рдгрд╛рд░реЗ. Self-hosted runner / agent = рддреБрдордЪреЗ рдорд╢реАрди рдХрд┐рдВрд╡рд╛ pod: рддреБрдордЪреЗ patching, рддреБрдордЪреА network рдкреЛрд╣реЛрдЪ, рддреБрдордЪреЗ рдмрд┐рд▓. рдЪрд╛рд░рд╣реА self-hosted рд▓рд╛ support рдХрд░рддрд╛рдд; Jenkins рддрд░ рдореБрд│рд╛рддрдЪ self-hosted рдЖрд╣реЗ.
ЁЯдФ рдХрд╛
Teams рднрд╛рд╖рд╛ рдмрджрд▓рддрд╛рдд рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рд╡реЗрд│рд╛ CI vendor рдмрджрд▓рддрд╛рдд, рдЖрдгрд┐ рдиреЛрдХрд░реАрдЪреНрдпрд╛ рдЬрд╛рд╣рд┐рд░рд╛рддреАрдВрдордзреНрдпреЗ
рдЪрд╛рд░рд╣реА рдирд╛рд╡реЗ рдЕрд╕рддрд╛рдд. рддреБрдордЪреНрдпрд╛ pipeline рдЪреЗ logic YAML рдордзреНрдпреЗ рдЕрд╕реЗрд▓ рддрд░ migration рдореНрд╣рдгрдЬреЗ
рдкреВрд░реНрдг рдкреБрдирд░реНрд▓реЗрдЦрди; рддреЗ scripts рдЖрдгрд┐ package.json рдордзреНрдпреЗ рдЕрд╕реЗрд▓ рддрд░ migration рдореНрд╣рдгрдЬреЗ
рдкрд╛рддрд│ рдЖрд╡рд░рдгрд╛рдЪреЗ рднрд╛рд╖рд╛рдВрддрд░ тАФ рдЬреЗ рдиреЗрдордХреЗ рд╡рд░рдЪреНрдпрд╛ рддрдХреНрддреНрдпрд╛рдд рдЖрд╣реЗ. рдпрд╛
рдХреЛрд░реНрд╕рдЪреЗ рдЪрд╛рд░ tab рдЕрд╕рд▓реЗрд▓реЗ рдкрд╛рди
рдкреВрд░реНрдг files рд╢реЗрдЬрд╛рд░реА рд╢реЗрдЬрд╛рд░реА рджрд╛рдЦрд╡рддреЗ.
рдкреНрд░рддреНрдпреЗрдХ рдХреБрдареЗ рдЪрдордХрддреЛ (рд╕рд╛рд╡рдзрдкрдгреЗ тАФ рдЪрд╛рд░рд╣реА рдмрджрд▓рдд рд░рд╛рд╣рддрд╛рдд)
- тЪЩя╕П GitHub Actions: code рдЖрдзреАрдЪ рдЬрд┐рдереЗ рдЖрд╣реЗ рддрд┐рдереЗрдЪ рд░рд╛рд╣рддреЛ; actions рдЪрд╛ рдореЛрдард╛ marketplace; рд╣реЗ рд▓рд┐рд╣рд┐рддрд╛рдирд╛ public repositories рдирд╛ рдореЛрдлрдд рдорд░реНрдпрд╛рджрд╛ рдорд┐рд│рддреЗ.
- ЁЯФД CircleCI: рд╡реЗрдЧрд╡рд╛рди executors, рдкреБрдиреНрд╣рд╛ рд╡рд╛рдкрд░рддрд╛ рдпреЗрдгрд╛рд░реЗ orbs, рдирд╛рд╡рд╛рдЪреНрдпрд╛ jobs рдкреБрд░рддреЗ
credentials рдорд░реНрдпрд╛рджрд┐рдд рдХрд░рдгрд╛рд░реЗ contexts, image builds рд╕рд╛рдареА
setup_remote_docker. - ЁЯжК GitLab CI: SCM рдЖрдгрд┐ CI рдПрдХрд╛рдЪ product рдордзреНрдпреЗ; environments рдЖрдгрд┐ merge-request widgets (JUnit report MR рдордзреНрдпреЗ рджрд┐рд╕рддреЛ); рд╕реНрд╡рддрдГ рдЪрд╛рд▓рд╡рд╛рдпрд▓рд╛ рд╕реЛрдкреЗ.
- ЁЯОй Jenkins: self-hosted, рдЬрд╡рд│рдЬрд╡рд│ рдкреНрд░рддреНрдпреЗрдХ рдЧреЛрд╖реНрдЯреАрд╕рд╛рдареА plugin, рдореЛрдареНрдпрд╛ рдХрд┐рдВрд╡рд╛ рджреАрд░реНрдШрдХрд╛рд│ рдЪрд╛рд▓рд▓реЗрд▓реНрдпрд╛ рд╡реНрдпрд╡рд╕реНрдерд╛рдВрдордзреНрдпреЗ рдЕрдЬреВрдирд╣реА рд╕рд╛рдорд╛рдиреНрдп; controller рдЖрдгрд┐ рддреНрдпрд╛рдЪреЗ upgrades рддреБрдордЪреНрдпрд╛ рдбреЛрдХреНрдпрд╛рд╡рд░.
рдкреНрд░рддреНрдпреЗрдХ 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
тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛
- рдЖрдзреА logic scripts рдордзреНрдпреЗ рди рд╣рд▓рд╡рддрд╛ YAML рдУрд│-рди-рдУрд│ рднрд╛рд╖рд╛рдВрддрд░ рдХрд░реВрди pipeline port рдХрд░рдгреЗ тАФ рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рдЪрд┐рддреНрд░рдкрдгрд╛ рддреБрдореНрд╣реА рд╕реЛрдмрдд рдиреЗрддрд╛ рдЖрдгрд┐ рдХрд╛рд╣реАрдЪ рдорд┐рд│рд╡рдд рдирд╛рд╣реА
- рд╢рдмреНрдж рдПрдХрд╛рд╕ рдПрдХ рдЬреБрд│рддрд╛рдд рдЕрд╕реЗ рдорд╛рдирдгреЗ: GitLab рдЪрд╛
stageparallel рдЪрд╛рд▓рдгрд╛рд▒реНрдпрд╛ jobs рдЪрд╛ рдЧрдЯ рдЕрд╕рддреЛ, Jenkins рдЪрд╛stageрдПрдХ рдХреНрд░рдорд╡рд╛рд░ block рдЕрд╕рддреЛ, рдЖрдгрд┐ CircleCI job рдЪрд╛ рдХреНрд░рдо рдлрдХреНрддrequires:рдиреЗ рдард░рддреЛ - рдорд╛рдЧрдЪреНрдпрд╛ рд╡рд░реНрд╖реАрдЪреНрдпрд╛ рдореЛрдлрдд рдорд░реНрдпрд╛рджреЗрд╡рд░реВрди рдХрд┐рдВрд╡рд╛ рдПрдЦрд╛рджреНрдпрд╛ blog post рд╡рд░реВрди vendors рдЪреА рддреБрд▓рдирд╛ рдХрд░рдгреЗ тАФ рдорд░реНрдпрд╛рджрд╛ рдмрджрд▓рддрд╛рдд; рдирд┐рд╡рдбрдгреНрдпрд╛рдЖрдзреА рд╕рдзреНрдпрд╛рдЪреЗ рдкрд╛рди рддрдкрд╛рд╕рд╛
тПня╕П рдкреБрдвреЗ
рддреБрдореНрд╣реА рдЪрд╛рд░рд╣реА рдлреЙрд░реНрдо рд╡рд╛рдЪреВ рд╢рдХрддрд╛. рд╢реЗрд╡рдЯрдЪрд╛ рдзрдбрд╛ рдореНрд╣рдгрдЬреЗ mailroom рдЪреЗ рдирд┐рдпрдордкреБрд╕реНрддрдХ poster: pipeline рд╡рд░реНрд╖рд╛рдиреБрд╡рд░реНрд╖реЗ рд╡рд┐рд╢реНрд╡рд╛рд╕рд╛рд░реНрд╣ рдХрд╢рд╛рдиреЗ рд░рд╛рд╣рддреЗ, рдЖрдгрд┐ ArgoCD рд╢рд╛рд│реЗрдХрдбреЗ рд╣рд╕реНрддрд╛рдВрддрд░.
git checkout lesson-12-pipeline-hygiene