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

ЁЯУО рдзрдбрд╛ 05 тАФ Artifacts рдЖрдгрд┐ test reports: рдбреЗрд╕реНрдХрд╛рдВрдордзреНрдпреЗ рдХрд╛рдп рдкреНрд░рд╡рд╛рд╕ рдХрд░рддреЗ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-cache-parallel-matrix ┬╖ рдкреБрдвреЗ: lesson-06-secrets-oidc


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

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ рдбреЗрд╕реНрдХ рджреБрд╕рд▒реНрдпрд╛ рдХреЛрдгрд╛рд▓рд╛ рджреЗрдК рд╢рдХрддреЗ рдЕрд╢рд╛ рджреЛрди рдЧреЛрд╖реНрдЯреА: рдорд╛рдгреВрд╕ рд╡рд╛рдЪрддреЛ рддреЛ test report рдЖрдгрд┐ рдирдВрддрд░рдЪреНрдпрд╛ job рд▓рд╛ рд▓рд╛рдЧрдгрд╛рд░реЗ artifact. рдЦрд▒реНрдпрд╛ files:

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

рддрдкрд╛рд╕рдгреА рдбреЗрд╕реНрдХрдиреЗ тЬЕ рддреБрдордЪрд╛ рдЧреГрд╣рдкрд╛рда рддрдкрд╛рд╕реВрди рд╕рдВрдкрд╡рд▓рд╛. рддреНрдпрд╛ рдбреЗрд╕реНрдХрд╡рд░реВрди рджреЛрди рдЧреЛрд╖реНрдЯреА рдмрд╛рд╣реЗрд░ рдкрдбрддрд╛рдд.

рдкрд╣рд┐рд▓реА, рдкреНрд░рдЧрддрд┐рдкреБрд╕реНрддрдХ ЁЯУЛ: "3 рддрдкрд╛рд╕рдгреНрдпрд╛, 3 рдкрд╛рд╕". рддреЗ рдЕрд╢рд╛ рдард┐рдХрд╛рдгреА рд▓рд╛рд╡рд▓реЗ рдЬрд╛рддреЗ рдЬрд┐рдереЗ рдХреЛрдгреАрд╣реА рд╡рд╛рдЪреВ рд╢рдХреЗрд▓ тАФ рд╢рд┐рдХреНрд╖рд┐рдХрд╛, рддреБрдордЪреЗ рдкрд╛рд▓рдХ, рдкреБрдврдЪреНрдпрд╛ рдордВрдЧрд│рд╡рд╛рд░реА рдХреЛрдгрддреА рддрдкрд╛рд╕рдгреА рдмрд┐рдШрдбрд▓реА рдпрд╛рдЪрд╛ рд╡рд┐рдЪрд╛рд░ рдХрд░рдгрд╛рд░реЗ рддреБрдореНрд╣реА. CI рдордзреНрдпреЗ рддреА JUnit XML file рдЕрд╕рддреЗ. CircleCI, GitLab рдЖрдгрд┐ Jenkins рддреА рд╡рд╛рдЪреВрди рдкреНрд░рддреНрдпреЗрдХ test рд╕рд╛рдареА рдПрдХ рдУрд│ рдЕрд╕рд▓реЗрд▓реЗ table рдмрдирд╡рддрд╛рдд; GitHub Actions рддреА download рдореНрд╣рдгреВрди рдареЗрд╡рддреЗ рдЬреА рддреБрдореНрд╣реА рд╕реНрд╡рддрдГ рдЙрдШрдбрддрд╛.

рджреБрд╕рд░реА, рдкрд╛рдХреАрдЯ ЁЯУО: рдлреЛрдЯреЛрдХреЙрдкреА рдбреЗрд╕реНрдХ ЁЯН▒ рддреБрдордЪреНрдпрд╛ рдЧреГрд╣рдкрд╛рдард╛рдЪреА рдкреНрд░рдд рдХрд╛рдврддреЛ рдЖрдгрд┐ рддреА delivery рдбреЗрд╕реНрдХрдХрдбреЗ рджреНрдпрд╛рдпрдЪреА рдЕрд╕рддреЗ. рдкрдг рддреЛ рдбреЗрд╕реНрдХ рд╡реЗрдЧрд│реНрдпрд╛ рдЦреЛрд▓реАрдд рдЖрд╣реЗ тАФ CI рдордзреНрдпреЗ рдкреНрд░рддреНрдпреЗрдХ job рдирд╡реНрдпрд╛ рдорд╢реАрдирд╡рд░ рдЙрддрд░рддреЛ, рдореНрд╣рдгреВрди рддреБрдореНрд╣реА рдмрдирд╡рд▓реЗрд▓реЗ рд╕рдЧрд│реЗ job рд╕рдВрдкрддрд╛рдХреНрд╖рдгреА рдирд╛рд╣реАрд╕реЗ рд╣реЛрддреЗ. рддреБрдореНрд╣реА рддреЗ рдкрд╛рдХрд┐рдЯрд╛рдд рдареЗрд╡рддрд╛, рддреНрдпрд╛рд╡рд░ рдирд╛рд╡ рд▓рд┐рд╣рд┐рддрд╛, рдЖрдгрд┐ run рдЪреНрдпрд╛ рдХрдкреНрдкреНрдпрд╛рдд рдЯрд╛рдХрддрд╛. рдкреБрдврдЪрд╛ рдбреЗрд╕реНрдХ рддреЗ рдирд╛рд╡рд╛рдиреЗ рдЙрдЪрд▓рддреЛ. рддреЗ рдкрд╛рдХреАрдЯ рдореНрд╣рдгрдЬреЗ artifact.

рдЖрдгрд┐ рдзрдбрд╛ 04 рдордзрд▓рд╛ рддрд╛рд╕рд▓реЗрд▓реНрдпрд╛ рдкреЗрдиреНрд╕рд┐рд▓реАрдВрдЪрд╛ рдбреНрд░реЙрд╡рд░ ЁЯЧДя╕П? рддреЛ рдореНрд╣рдгрдЬреЗ cache. рд╢рд┐рдкрд╛рдпрд╛рдиреЗ рдбреНрд░реЙрд╡рд░ рд░рд┐рдХрд╛рдорд╛ рдХреЗрд▓рд╛ рддрд░ рдХреЛрдгрд╛рд▓рд╛рдЪ рддреНрдпрд╛рдЪреА рдЙрдгреАрд╡ рднрд╛рд╕рдгрд╛рд░ рдирд╛рд╣реА тАФ рддреБрдореНрд╣реА рдлрдХреНрдд рдкреБрдиреНрд╣рд╛ рддрд╛рд╕рд╛рд▓. рдкрд╛рдХреАрдЯ рд╣рд░рд╡рдгреЗ рдореНрд╣рдгрдЬреЗ deliverable рд╣рд░рд╡рдгреЗ. рддреАрдЪ рдлрд│реА, рдЕрдЧрджреА рд╡реЗрдЧрд│реЗ рдирд┐рдпрдо.

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

flowchart LR
    t["тЬЕ test job<br/>node --test тЖТ test-results.xml"]
    rc["ЁЯУЛ report card<br/>artifact test-results-node22"]
    human["ЁЯзСтАНЁЯПл a human downloads it<br/>gh run download"]
    b["ЁЯН▒ build job<br/>docker save тЖТ image.tar.gz"]
    env["ЁЯУО envelope<br/>artifact image"]
    p["ЁЯЪЪ push job<br/>another machine, docker load"]
    t -->|"1 upload, even on failure"| rc
    rc -->|"2 read later"| human
    b -->|"3 upload"| env
    env -->|"4 download by name"| p

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

ЁЯза рдкрд╛рдХреАрдЯ рд╡рд┐рд░реБрджреНрдз рдбреНрд░реЙрд╡рд░

ЁЯУО artifact ЁЯЧДя╕П cache
рддреЗ рдХрд╛рдп рдЖрд╣реЗ рдпрд╛ run рдЪрд╛ output рдирдВрддрд░рдЪреНрдпрд╛ runs рд╕рд╛рдареА рд╡реЗрдЧ рд╡рд╛рдврд╡рдгрд╛рд░реЗ
рддреЗ рдХреЛрдг рд╡рд╛рдЪрддреЗ рдорд╛рдгрд╕реЗ, рдирдВрддрд░рдЪреЗ jobs, рдЗрддрд░ workflows рддреАрдЪ step, рдкреБрдврдЪреНрдпрд╛ рд╡реЗрд│реА
рддреЗ рдирд╛рд╣реАрд╕реЗ рдЭрд╛рд▓реЗ рддрд░ deliverable рд╣рд░рд╡рддреЗ тАФ рдПрдХ bug step рдлрдХреНрдд рд╕рд╛рд╡рдХрд╛рд╢ рдЪрд╛рд▓рддреЗ
рдУрд│рдЦ рдХрд╢рд╛рд╡рд░реВрди рдПрдХрд╛ run рдордзрд▓реЗ рдирд╛рд╡ inputs рд╡рд░реВрди hash рдХреЗрд▓реЗрд▓реА key

ЁЯдФ рдХрд╛

рдкреНрд░рдЧрддрд┐рдкреБрд╕реНрддрдХрд╛рд╢рд┐рд╡рд╛рдп рд▓рд╛рд▓ run рдореНрд╣рдгрдЬреЗ log рдЪреНрдпрд╛ рдордЬрдХреБрд░рд╛рдЪреА рднрд┐рдВрдд, рдЖрдгрд┐ "рдХреЛрдгрддреА test?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рдореНрд╣рдгрдЬреЗ scroll рдХрд░рдд рд░рд╛рд╣рдгреЗ. Artifact рд╢рд┐рд╡рд╛рдп push job рд▓рд╛ image рдкреБрдиреНрд╣рд╛ build рдХрд░рд╛рд╡реА рд▓рд╛рдЧрддреЗ тАФ рдЖрдгрд┐ рдордЧ рддреБрдореНрд╣реА push рдХрд░рддрд╛ рддреЛ рдбрдмрд╛ рддреБрдореНрд╣реА smoke-test рдХреЗрд▓реЗрд▓рд╛ рдбрдмрд╛ рдирд╕рддреЛ. Reports failures рд╡рд╛рдЪрдиреАрдп рдХрд░рддрд╛рдд; artifacts рд╣рд╕реНрддрд╛рдВрддрд░рдг рдкреНрд░рд╛рдорд╛рдгрд┐рдХ рдареЗрд╡рддрд╛рдд. Output рд╡рд┐рд░реБрджреНрдз рд╡реЗрдЧ рд╡рд╛рдврд╡рдгрд╛рд░реЗ рд╣реА рд╣реАрдЪ рд░реЗрд╖рд╛ Docker рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдзрдбрд╛ 02 рдордзрд▓реНрдпрд╛ Docker layer cache рдордзреВрдирд╣реА рдЬрд╛рддреЗ: cache рд╕реНрд╡рд╛рдЧрддрд╛рд░реНрд╣ рдЖрд╣реЗ, рдкрдг рдкреБрдврдЪреЗ рдХрд╛рд╣реАрд╣реА рддреЛ рдЕрд╕рдгреНрдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЕрд╕реВ рдирдпреЗ.

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

рдкреНрд░рдЧрддрд┐рдкреБрд╕реНрддрдХ. Node рдЪрд╛ test runner рдПрдХрд╛рдЪ рд╡реЗрд│реА рджреЛрди reporters рд▓рд┐рд╣реВ рд╢рдХрддреЛ тАФ рддреБрдордЪреНрдпрд╛ terminal рд╡рд░ рдорд╛рдгрд╕рд╛рд▓рд╛ рд╕реЛрдкрд╛ spec рдЖрдгрд┐ file рдордзреНрдпреЗ junit. app/package.json рдордзреВрди:

"test:ci": "node --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=test-results.xml"

ci.yml рддреАрдЪ command рдЪрд╛рд▓рд╡рддреЗ, рдордЧ file upload рдХрд░рддреЗ тАФ tests fail рдЭрд╛рд▓реНрдпрд╛ рддрд░реАрд╣реА, рдЖрдгрд┐ рдиреЗрдордХреЗ рддреЗрд╡реНрд╣рд╛рдЪ рддреБрдореНрд╣рд╛рд▓рд╛ рддреА рд╣рд╡реА рдЕрд╕рддреЗ:

      - name: ЁЯзк test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml

      - name: ЁЯУО upload the report card (lesson 05)
        if: always()          # especially when tests FAIL тАФ that's when you need the report
        uses: actions/upload-artifact@v4
        with:
          name: test-results-node${{ matrix.node }}
          path: app/test-results.xml
          retention-days: 7

рджреЛрди рддрдкрд╢реАрд▓ рдерд╛рдВрдмреВрди рдкрд╛рд╣рдгреНрдпрд╛рд╕рд╛рд░рдЦреЗ. if: always() рдирд╕реЗрд▓ рддрд░ fail рд╣реЛрдгрд╛рд░реА test step job рдерд╛рдВрдмрд╡рддреЗ рдЖрдгрд┐ upload рд╡рдЧрд│рд▓рд╛ рдЬрд╛рддреЛ тАФ report рдлрдХреНрдд рд╣рд┐рд░рд╡реНрдпрд╛ runs рд╕рд╛рдареАрдЪ рдЕрд╕реЗрд▓. рдЖрдгрд┐ artifact рдЪреНрдпрд╛ рдирд╛рд╡рд╛рдд Node version рдЖрд╣реЗ рдХрд╛рд░рдг matrix рдЪреЗ рддреАрдирд╣реА рднрд╛рдЧ рдкреНрд░рддреНрдпреЗрдХреА рдПрдХ рдмрдирд╡рддрд╛рдд; upload-artifact@v4 рдордзреНрдпреЗ рдПрдХ artifact рдирд╛рд╡ рдПрдХрд╛ run рдордзреНрдпреЗ рдПрдХрджрд╛рдЪ рд╡рд╛рдкрд░рддрд╛ рдпреЗрддреЗ, рдореНрд╣рдгреВрди рд╕рд╛рдорд╛рдпрд┐рдХ рдирд╛рд╡ рдЕрд╕реЗрд▓ рддрд░ рджреБрд╕рд▒реНрдпрд╛ рдлреВрдЯрдкрдЯреНрдЯреАрдЪрд╛ upload fail рд╣реЛрдИрд▓.

рдбреЗрд╕реНрдХрд╛рдВрдордзрд▓реЗ рдкрд╛рдХреАрдЯ. ship.yml рдордзреНрдпреЗ build job рдиреБрдХрддреАрдЪ smoke-test рдХреЗрд▓реЗрд▓реА image рдмрд╛рдВрдзрддреЛ, рдЖрдгрд┐ push job тАФ рд╡реЗрдЧрд│реА рдорд╢реАрди тАФ рддреЗрдЪ bytes рдЙрдШрдбрддреЛ:

      - name: ЁЯТ╛ keep the image for the push job (same run, different machine)
        run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz
      - uses: actions/upload-artifact@v4
        with: { name: image, path: image.tar.gz, retention-days: 1 }
    steps:
      - uses: actions/download-artifact@v4
        with: { name: image }
      - run: docker load < image.tar.gz

рдПрдХрд╛ рджрд┐рд╡рд╕рд╛рдЪреЗ retention рдкреБрд░реЗрд╕реЗ рдЖрд╣реЗ: push рдХрд╛рд╣реА рдорд┐рдирд┐рдЯрд╛рдВрдиреАрдЪ рд╣реЛрддреЛ, рдЖрдгрд┐ image рдЪреЗ рджреАрд░реНрдШрдХрд╛рд▓реАрди рдШрд░ ECR рдЖрд╣реЗ, artifact store рдирд╛рд╣реА (рдзрдбрд╛ 07). retention-days рдирд╕реЗрд▓ рддрд░ GitHub artifacts repository рдЪреНрдпрд╛ default рдиреБрд╕рд╛рд░ рдареЗрд╡рддреЛ тАФ Settings тЖТ Actions рдордзреНрдпреЗ рдХреЛрдгреА рдмрджрд▓рд▓реЗ рдирд╕реЗрд▓ рддрд░ 90 рджрд┐рд╡рд╕.

ЁЯФБ рд╣реЗрдЪ CircleCI рдордзреНрдпреЗ
      - 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
      - store_test_results:                                   # lesson 05: the report card, parsed
          path: test-results
      - store_artifacts:                                      # тАжand the raw file, downloadable
          path: test-results

store_test_results Tests tab рд▓рд╛ рдорд╛рд╣рд┐рддреА рдкреБрд░рд╡рддреЗ (рдЖрдгрд┐ CircleCI рдЪреНрдпрд╛ рд╡реЗрд│реЗрд╡рд░ рдЖрдзрд╛рд░рд┐рдд test splitting рд▓рд╛); store_artifacts рдХрдЪреНрдЪреА XML Artifacts рдЦрд╛рд▓реА рдареЗрд╡рддреЗ. Jobs рджрд░рдореНрдпрд╛рди CircleCI files persist_to_workspace / attach_workspace рдиреЗ рджреЗрддреЗ (# illustrative тАФ рдЗрдереЗ рдЧрд░рдЬ рдирд╛рд╣реА, рдХрд╛рд░рдг рдпрд╛ config рдордзреНрдпреЗ build рдЖрдгрд┐ push рдПрдХрдЪ job рд╡рд╛рдЯреВрди рдШреЗрддрд╛рдд).

ЁЯжК рд╣реЗрдЪ GitLab CI рдордзреНрдпреЗ
  artifacts:                                  # lesson 05: the report card тАФ parsed into the MR widget
    when: always
    reports:
      junit: app/test-results.xml
    paths: [app/test-results.xml]
    expire_in: 1 week

reports: junit merge request рд╡рд░ test summary widget рдХрд╛рдврддреЗ ("1 failed, 2 passed"). paths: рд╢рд┐рд╡рд╛рдп file download рдХрд░рдгреНрдпрд╛рдЬреЛрдЧреА рдХрд░рддреЗ, when: always рдореНрд╣рдгрдЬреЗ if: always(), рдЖрдгрд┐ expire_in рдореНрд╣рдгрдЬреЗ retention. рдирдВрддрд░рдЪреНрдпрд╛ stages рдордзрд▓реНрдпрд╛ jobs рдирд╛ рдЖрдзреАрдЪреНрдпрд╛ jobs рдЪреЗ artifacts: default рдиреБрд╕рд╛рд░ рдорд┐рд│рддрд╛рдд.

ЁЯОй рд╣реЗрдЪ Jenkins рдордзреНрдпреЗ
            post { always { junit 'app/test-results.xml' } }   // lesson 05: the report card

junit (JUnit plugin) file рд╡рд╛рдЪреВрди build рдЪреЗ test result page рдЖрдгрд┐ trend graph рдмрдирд╡рддреЗ, рдЖрдгрд┐ test fail рдЭрд╛рд▓реА рддрд░ build рд▓рд╛ failed рдРрд╡рдЬреА unstable рдЕрд╢реА рдЦреВрдг рдХрд░рддреЗ. рдХрдЪреНрдЪреА file рд╕реБрджреНрдзрд╛ рдареЗрд╡рд╛рдпрдЪреА рдЕрд╕реЗрд▓ рддрд░:

archiveArtifacts artifacts: 'app/test-results.xml'   // illustrative

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

# 0) in your fork (lesson 03): make sure at least one тЬЕ ci run exists
gh run list --workflow ci.yml --limit 3

# 1) grab the newest run's id and download the Node 22 report card
RUN=$(gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run download "$RUN" -n test-results-node22 -D report
cat report/test-results.xml         # <testsuite тАж tests="3" failures="0" тАж> тАФ one <testcase> per test

# 2) produce the same file locally тАФ no CI needed
cd app && npm run test:ci && head -5 test-results.xml && cd ..

# 3) see the envelope that ship.yml builds (Actions тЖТ ЁЯУж ship тЖТ a run тЖТ Artifacts тЖТ "image")
gh run list --workflow ship.yml --limit 1
# it is tens of MB gzipped and disappears after a day тАФ that's retention-days: 1 at work

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

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

Push job рдЖрддрд╛ AWS рд╢реА рдмреЛрд▓рдгрд╛рд░ рдЖрд╣реЗ. рддреНрдпрд╛рдиреЗ рд╢рд╛рд│реЗрдЪреА рдореБрдЦреНрдп рдХрд┐рд▓реНрд▓реА ЁЯФС рдЦрд┐рд╢рд╛рдд рдШреЗрдКрди рдлрд┐рд░реВ рдирдпреЗ тАФ рддреНрдпрд╛рдиреЗ рдмреЕрдЬ рджрд╛рдЦрд╡реВрди рдбреЗ рдкрд╛рд╕ рдШреНрдпрд╛рд╡рд╛. ЁЯкк

git checkout lesson-06-secrets-oidc

ЁЯУО Lesson 05 тАФ Artifacts & test reports: what travels between desks

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-cache-parallel-matrix ┬╖ Next: lesson-06-secrets-oidc


ЁЯУж What's in this branch

Lessons 01тАУ04, plus the two things a desk can hand to someone else: a test report a human reads and an artifact a later job needs. Real files:

ЁЯзТ Explain like I'm 5

The checking desk тЬЕ finished your homework. Two things leave that desk.

First, the report card ЁЯУЛ: "3 checks, 3 passed". It gets pinned where anyone can read it тАФ the teacher, your parents, you next Tuesday when you wonder which check broke. In CI that's a JUnit XML file. CircleCI, GitLab and Jenkins parse it into a table with a row per test; GitHub Actions keeps it as a download you open yourself.

Second, the envelope ЁЯУО: the photocopier desk ЁЯН▒ makes a copy of your homework and has to hand it to the delivery desk. But that desk is in a different room тАФ in CI each job lands on a fresh machine, so anything you made is gone the moment the job ends. You put it in an envelope, write a name on it, and drop it in the run's pigeonhole. The next desk picks it up by name. That envelope is an artifact.

And the drawer of sharpened pencils ЁЯЧДя╕П from lesson 04? That's a cache. Nobody would miss the drawer if the janitor emptied it тАФ you'd just sharpen again. An envelope going missing is a lost deliverable. Same shelf, very different rules.

ЁЯЧ║я╕П Diagram

flowchart LR
    t["тЬЕ test job<br/>node --test тЖТ test-results.xml"]
    rc["ЁЯУЛ report card<br/>artifact test-results-node22"]
    human["ЁЯзСтАНЁЯПл a human downloads it<br/>gh run download"]
    b["ЁЯН▒ build job<br/>docker save тЖТ image.tar.gz"]
    env["ЁЯУО envelope<br/>artifact image"]
    p["ЁЯЪЪ push job<br/>another machine, docker load"]
    t -->|"1 upload, even on failure"| rc
    rc -->|"2 read later"| human
    b -->|"3 upload"| env
    env -->|"4 download by name"| p

тЭУ What

ЁЯза Envelope vs drawer

ЁЯУО artifact ЁЯЧДя╕П cache
what it is an output of this run a speed-up for later runs
who reads it humans, later jobs, other workflows the same step, next time
if it vanishes a deliverable is missing тАФ a bug the step just runs slower
addressed by a name inside one run a key hashed from the inputs

ЁЯдФ Why

Without the report card, a red run is a wall of log text and the answer to "which test?" is scrolling. Without the artifact, the push job has to rebuild the image тАФ and then the box you push is not the box you smoke-tested. Reports make failures readable; artifacts make handoffs honest. The same output-vs-speed-up line runs through the Docker layer cache in the Docker school's lesson 02: a cache is welcome, but nothing downstream should depend on it existing.

ЁЯФз How (in this repo)

The report card. Node's test runner can write two reporters at once тАФ the human-friendly spec to your terminal and junit to a file. From app/package.json:

"test:ci": "node --test --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=test-results.xml"

ci.yml runs the same command, then uploads the file тАФ even when the tests fail, which is exactly when you want it:

      - name: ЁЯзк test
        run: >
          node --test
          --test-reporter=spec  --test-reporter-destination=stdout
          --test-reporter=junit --test-reporter-destination=test-results.xml

      - name: ЁЯУО upload the report card (lesson 05)
        if: always()          # especially when tests FAIL тАФ that's when you need the report
        uses: actions/upload-artifact@v4
        with:
          name: test-results-node${{ matrix.node }}
          path: app/test-results.xml
          retention-days: 7

Two details worth a pause. Without if: always() a failing test step stops the job and the upload is skipped тАФ the report would exist only for green runs. And the artifact name includes the Node version because the three matrix legs each produce one; with upload-artifact@v4 an artifact name can be used once per run, so a shared name would make the second ruler's upload fail.

The envelope between desks. In ship.yml the build job packs the image it just smoke-tested, and the push job тАФ a separate machine тАФ unpacks the very same bytes:

      - name: ЁЯТ╛ keep the image for the push job (same run, different machine)
        run: docker save hello-courier:${GITHUB_SHA} | gzip > image.tar.gz
      - uses: actions/upload-artifact@v4
        with: { name: image, path: image.tar.gz, retention-days: 1 }
    steps:
      - uses: actions/download-artifact@v4
        with: { name: image }
      - run: docker load < image.tar.gz

One day of retention is plenty: the push happens minutes later, and the long-term home of the image is ECR, not the artifact store (lesson 07). Without retention-days GitHub keeps artifacts for the repository's default тАФ 90 days unless someone changed it under Settings тЖТ Actions.

ЁЯФБ The same thing in CircleCI
      - 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
      - store_test_results:                                   # lesson 05: the report card, parsed
          path: test-results
      - store_artifacts:                                      # тАжand the raw file, downloadable
          path: test-results

store_test_results feeds the Tests tab (and CircleCI's timing-based test splitting); store_artifacts keeps the raw XML under Artifacts. Between jobs CircleCI passes files with persist_to_workspace / attach_workspace (# illustrative тАФ not needed here, because build and push share one job in this config).

ЁЯжК The same thing in GitLab CI
  artifacts:                                  # lesson 05: the report card тАФ parsed into the MR widget
    when: always
    reports:
      junit: app/test-results.xml
    paths: [app/test-results.xml]
    expire_in: 1 week

reports: junit is what draws the test summary widget on a merge request ("1 failed, 2 passed"). paths: additionally makes the file downloadable, when: always is the if: always(), and expire_in is the retention. Jobs in later stages receive earlier jobs' artifacts: by default.

ЁЯОй The same thing in Jenkins
            post { always { junit 'app/test-results.xml' } }   // lesson 05: the report card

junit (JUnit plugin) parses the file into the build's test result page and trend graph, and marks the build unstable rather than failed when a test fails. To keep the raw file too:

archiveArtifacts artifacts: 'app/test-results.xml'   // illustrative

ЁЯзк Try it

# 0) in your fork (lesson 03): make sure at least one тЬЕ ci run exists
gh run list --workflow ci.yml --limit 3

# 1) grab the newest run's id and download the Node 22 report card
RUN=$(gh run list --workflow ci.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run download "$RUN" -n test-results-node22 -D report
cat report/test-results.xml         # <testsuite тАж tests="3" failures="0" тАж> тАФ one <testcase> per test

# 2) produce the same file locally тАФ no CI needed
cd app && npm run test:ci && head -5 test-results.xml && cd ..

# 3) see the envelope that ship.yml builds (Actions тЖТ ЁЯУж ship тЖТ a run тЖТ Artifacts тЖТ "image")
gh run list --workflow ship.yml --limit 1
# it is tens of MB gzipped and disappears after a day тАФ that's retention-days: 1 at work

тЪая╕П Common mistakes

тПня╕П Next

The push job is about to talk to AWS. It should not be carrying the school's master key ЁЯФС in its pocket тАФ it should show a badge and get a day pass. ЁЯкк

git checkout lesson-06-secrets-oidc
тЖР Previouscache parallel matrixNext тЖТsecrets oidc

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