Jenkins Learn
06. Tích hợp công cụ

Artifact repositories với Jenkins

Publish package bất biến từ Jenkins với credential tối thiểu, checksum, retention và promotion có thể truy vết.

Phạm vi và điều kiện

Bài này mô tả contract giữa Jenkins và một artifact repository. Ví dụ HTTP cần HTTP Request Plugin được phê duyệt; Maven, npm và OCI dùng client/protocol riêng. Không có snippet nào tạo repository, credential hay thao tác lên dịch vụ production.

Artifact repository là nơi phân phối output đã được kiểm tra, không phải workspace hay cache của agent. Một release đáng tin phải có version hoặc digest bất biến, checksum, metadata nguồn và policy ngăn ghi đè. Jenkins chỉ publish sau gate tin cậy; repository vẫn phải tự enforce quyền và immutability.

Mục lục

Mục tiêu và điều kiện trước

Sau bài này, bạn có thể chọn nơi publish phù hợp, liên kết một package với revision đã build, và tránh cấp token release cho code chưa tin cậy. Trước khi cấu hình Pipeline, chốt các thông tin sau với owner repository:

  • format distribution: generic HTTP, Maven, npm, OCI hay object storage;
  • namespace/repository đích, naming convention và version bất biến;
  • service identity nào chỉ được ghi, identity nào chỉ được đọc, cùng ngày rotation;
  • CA/TLS, proxy/egress allowlist, quota, audit log và thời hạn retention;
  • policy xử lý package đã tồn tại, timeout upload, promotion và rollback.

Không dùng repository để che failure

Một upload thành công không chứng minh artifact an toàn. Test, scan, checksum và policy phải đạt trước publish. Ngược lại, archive Jenkins không thay thế repository phát hành; nó chỉ là evidence theo từng build.

Chọn repository và giao thức

Ma trận quyết định

Nexus Repository và JFrog Artifactory là các sản phẩm quản lý nhiều format. Object storage là lớp object/key cần policy naming, conditional write và lifecycle do đội vận hành thiết kế. Không có lựa chọn nào tự biến mọi bytes thành package an toàn.

Nhu cầuGiao thức và identity artifactRepository phù hợpKiểm soát cần xác minh
File generic, ví dụ .tgz hoặc report đã kýHTTPS PUT vào path version cố định; SHA-256 đi kèmGeneric repository của Nexus/Artifactory hoặc object storageKhông ghi đè key release, conditional write, checksum metadata, ACL path và lifecycle
JavaMaven coordinate groupId:artifactId:versionMaven hosted repositorydeploy chỉ vào namespace cần thiết, release không redeploy, checksum và mirror TLS
Node.jsPackage name và version trong package.jsonnpm hosted registryVersion package không publish lại, dist-tag có owner, token publish tách token install
ContainerOCI manifest digestOCI registryDeploy/promotion theo repository@sha256:..., tag không là identity release, signature/referrer policy
Lưu archive Jenkins ra ngoàiArtifact Manager backend, không phải package registryObject storage qua plugin đã phê duyệtIAM bucket, encryption, lifecycle, plugin/core compatibility và restore

Maven, npm và OCI có client cùng metadata riêng. Không dùng generic HTTP upload để giả lập mvn deploy, npm publish hay OCI push; những client này còn thực hiện validation, layout và authentication theo protocol. Chọn plugin hoặc client chính thức của format đang phát hành, pin version của nó trong image agent/catalog tool và kiểm tra trên sandbox.

Repository không thay thế archive Jenkins

Nơi lưuDùng đúng choKhông dùng cho
Workspace agentCheckout, build và file tạmTruyền release giữa build hoặc giữ lâu dài
stashDi chuyển file nhỏ đã biết giữa stage cùng một runRepository phát hành hay cache dùng chung
archiveArtifactsReport, checksum và evidence của một Jenkins buildKho distribution dài hạn cho consumer
Artifact repositoryConsumer resolve package/version bất biến, promotion và rollbackThay thế build log, quyền Jenkins hoặc test gate

Khi consumer tải artifact, nó phải xác minh SHA-256 hoặc digest từ kênh tin cậy trước khi chạy/deploy. Jenkins fingerprint hỗ trợ truy vết nội bộ nhưng dùng MD5 lịch sử, nên không thay checksum mật mã hoặc signature. Xem Build Artifacts để tách bốn vòng đời này.

Credential và ranh giới tin cậy

Quyền tối thiểu và TLS

Credential ID có thể ở Jenkinsfile; giá trị secret phải ở Jenkins Credentials hoặc secret manager đã phê duyệt. Cấp một publisher identity chỉ có quyền ghi vào repository/path của sản phẩm và không có quyền xóa, cấu hình repository hay quản trị tổ chức. Consumer dùng identity read-only tách biệt. Domain hiển thị credential trong UI không phải security boundary; scope folder/job, permission Jenkins, token scope và egress mới là boundary thực.

Dùng TLS với hostname và CA do tổ chức tin cậy. Không đặt ignoreSslErrors: true, không dùng curl với bỏ qua certificate verification, và không chữa lỗi CA bằng cách chuyển endpoint sang HTTP. Khi token/CA/endpoint đổi, thử publish package vô hại trên sandbox, cập nhật consumer theo change record, rồi revoke phiên bản credential cũ sau overlap window đã định.

Có hai cách nạp quyền thường gặp:

  • Credential-aware step: HTTP Request Plugin nhận authentication: 'artifact-release-publisher'; secret không đi qua Groovy, shell hay argv. Đây là lựa chọn của ví dụ generic HTTP bên dưới.
  • Client đã hỗ trợ credential store/config an toàn: Maven settings.xml, npm configuration hoặc OCI client phải nhận secret qua binding ngắn do plugin/tool hỗ trợ. Không đưa token vào URL query, header do shell ghép, source, log, report, cache hay artifact.

Masking console chỉ giảm lộ vô tình. Code, dependency hoặc process đã nhận secret vẫn có thể gửi nó ra ngoài. Vì vậy credential release chỉ được bind trong stage release ngắn nhất trên agent tách biệt.

Pull request và release

LuồngAgent và quyềnKết quả được phép
Pull request/forkPool untrusted-pr, không credential publish, cache không ghi vào tier releaseBuild, test, tạo checksum/SBOM local và archive evidence không nhạy cảm
Branch nội bộ đã reviewPool CI có quyền read cần thiếtCandidate có thể được kiểm tra theo policy riêng
main hoặc tag release được bảo vệPool trusted-release, credential write scope hẹpPublish artifact đã kiểm tra, đọc lại metadata/digest, tạo record promotion

Label chỉ điều phối agent; nó không tự ngăn người sửa Jenkinsfile dùng credential. Multibranch trust policy, branch protection, authorization, network, workspace/cache isolation và scope credential phải cùng bảo vệ stage release. Chi tiết binding nằm tại Credentials trong Pipeline và ranh giới agent tại Bảo mật Agent và Plugin.

Artifact bất biến và version strategy

Checksum digest và provenance

Mỗi release cần có một identity bất biến và evidence đi kèm:

EvidenceMục đíchKhông phải
SHA-256 hoặc SHA-512Xác nhận bytes package khi upload/downloadJenkins fingerprint MD5
Version hoặc OCI digestChỉ đúng artifact để consumer/promotion dùngMột tag hay alias có thể bị đổi
SBOMInventory dependency và thành phầnKết luận không có mọi vulnerability
Provenance/attestation theo policyLiên kết source, builder và artifactMột JSON tự tạo được gọi là chứng nhận chuẩn
Repository audit eventAi publish path/version nào, lúc nàoThay thế checksum hoặc review release

Tạo checksum sau package và archive cùng package, SBOM/metadata theo allowlist hẹp. Repository phải từ chối overwrite version release. Nếu upload trả 409 hoặc lỗi “already exists”, dừng và đọc metadata/checksum: chỉ coi thao tác idempotent khi server xác nhận bytes giống hệt. Không xóa hoặc overwrite release để thử lại.

Version snapshot promotion và rollback

Release version như 1.4.0 là immutable. Candidate như 1.4.1-rc.2 cần namespace/retention riêng. Snapshot là artifact thay đổi theo policy hoặc timestamp nên không phải identity rollback cuối cùng; Maven snapshot, npm dist-tag và OCI tag có semantics khác nhau, cần owner của format đó phê duyệt.

Promotion chuyển cùng bytes từ candidate sang release hoặc thay trạng thái repository theo capability của provider. Nó không build lại từ checkout mới. Record promotion cần gồm version/digest, SHA-256, source revision, build URL, policy/approval và artifact rollback trước. Rollback chọn version/digest tốt đã biết, xác minh checksum rồi để consumer/deployer nhận đúng identity đó.

Không dùng alias hoặc tag di động làm release input. Retention cũng không được xóa artifact còn trong manifest môi trường, rollback window, legal hold hoặc consumer support policy.

Jenkins Pipeline publish an toàn

Giả định plugin và contract

Ví dụ dùng Jenkins LTS, Pipeline: Declarative, Git, JUnit, Pipeline: Basic Steps, Python 3 trên agent và HTTP Request Plugin đã được đội platform pin/review. HTTP Request Plugin phải hỗ trợ authentication, customHeaders, uploadFile, outputFile, validResponseCodes và TLS verification trên version đang chạy; kiểm tra lại bằng Pipeline Syntax → Snippet Generator. Repository generic HTTP phải honor If-None-Match: * và chỉ làm object nhìn thấy sau khi một PUT hoàn tất. timestamps() cần Timestamper.

Pipeline giả định stage package đã tạo đúng ba file trong dist/release/: widget-1.4.0.tgz, widget-1.4.0.tgz.sha256 và build-metadata.json. Sidecar checksum dùng tên file widget-1.4.0.tgz để sha256sum -c xác minh từ cùng directory. Metadata là JSON có subject.name, subject.version, subject.sha256 lần lượt bằng widget-1.4.0.tgz, 1.4.0 và SHA-256 của package; nó có thể thêm source revision/toolchain theo policy. Đây là contract output của dự án, không phải lệnh build chung cho mọi ngôn ngữ. Agent release đã có CA/network route tới repository; URL .invalid trong mẫu chỉ biểu diễn endpoint sandbox và không phải destination để chạy.

Jenkinsfile dùng credential-aware step

Snippet archive evidence trước cleanup và chỉ publish khi Multibranch đang build main, không phải change request. Nó tạo sidecar SHA-256 và metadata trước, đọc lại/verify hai sidecar, rồi mới upload package như publication marker cuối cùng. Consumer chỉ coi release tồn tại khi package marker và cả hai sidecar cùng có mặt, có SHA-256 và subject/version/digest khớp. httpRequest(authentication: ...) để plugin lấy credential trực tiếp từ Jenkins store; Jenkinsfile không mở secret vào shell/Groovy.

pipeline {
  agent none

  options {
    skipDefaultCheckout(true)
    timestamps()
    timeout(time: 25, unit: 'MINUTES')
    buildDiscarder(logRotator(
      daysToKeepStr: '30', numToKeepStr: '30',
      artifactDaysToKeepStr: '14', artifactNumToKeepStr: '10'
    ))
  }

  environment {
    ARTIFACT_FILE = 'dist/release/widget-1.4.0.tgz'
    ARTIFACT_SHA256 = 'dist/release/widget-1.4.0.tgz.sha256'
    ARTIFACT_METADATA = 'dist/release/build-metadata.json'
    REPOSITORY_ARTIFACT_URL = 'https://repository.example.invalid/generic-releases/acme/widget/1.4.0/widget-1.4.0.tgz'
    REPOSITORY_SHA256_URL = 'https://repository.example.invalid/generic-releases/acme/widget/1.4.0/widget-1.4.0.tgz.sha256'
    REPOSITORY_METADATA_URL = 'https://repository.example.invalid/generic-releases/acme/widget/1.4.0/build-metadata.json'
  }

  stages {
    stage('Checkout test package') {
      agent { label 'linux && artifact-builder' }
      steps {
        checkout scm
        sh '''#!/usr/bin/env sh
          set -eu
          # Contract dự án: test/package đã review tạo đúng output release.
          ./ci/test-and-package
          test -s "$ARTIFACT_FILE"
          test -s "$ARTIFACT_SHA256"
          test -s "$ARTIFACT_METADATA"
          (cd dist/release && sha256sum -c widget-1.4.0.tgz.sha256)
          python3 - "$ARTIFACT_FILE" "$ARTIFACT_SHA256" "$ARTIFACT_METADATA" <<'PY'
import hashlib
import json
from pathlib import Path
import sys

artifact, checksum_file, metadata_file = map(Path, sys.argv[1:])
expected = checksum_file.read_text(encoding='utf-8').split()[0]
actual = hashlib.sha256(artifact.read_bytes()).hexdigest()
metadata = json.loads(metadata_file.read_text(encoding='utf-8'))
subject = metadata.get('subject', {})
assert expected == actual
assert subject == {
    'name': artifact.name,
    'version': '1.4.0',
    'sha256': expected,
}
PY
        '''
        stash includes: 'dist/release/widget-1.4.0.tgz,dist/release/widget-1.4.0.tgz.sha256,dist/release/build-metadata.json',
          name: 'verified-release'
      }
      post {
        always {
          archiveArtifacts artifacts: 'dist/release/widget-1.4.0.tgz,dist/release/widget-1.4.0.tgz.sha256,dist/release/build-metadata.json',
            allowEmptyArchive: true, fingerprint: true
          deleteDir()
        }
      }
    }

    stage('Publish immutable release') {
      when {
        beforeAgent true
        allOf {
          branch 'main'
          not { changeRequest() }
        }
      }
      agent { label 'linux && trusted-release' }
      steps {
        unstash 'verified-release'
        sh '''#!/usr/bin/env sh
          set -eu
          (cd dist/release && sha256sum -c widget-1.4.0.tgz.sha256)
        '''
        script {
          def checksumResponse = httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            contentType: 'TEXT_PLAIN',
            customHeaders: [[maskValue: false, name: 'If-None-Match', value: '*']],
            httpMode: 'PUT',
            ignoreSslErrors: false,
            timeout: 30,
            uploadFile: env.ARTIFACT_SHA256,
            url: env.REPOSITORY_SHA256_URL,
            validResponseCodes: '200:201,204,412'
          )
          def metadataResponse = httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            contentType: 'APPLICATION_JSON',
            customHeaders: [[maskValue: false, name: 'If-None-Match', value: '*']],
            httpMode: 'PUT',
            ignoreSslErrors: false,
            timeout: 30,
            uploadFile: env.ARTIFACT_METADATA,
            url: env.REPOSITORY_METADATA_URL,
            validResponseCodes: '200:201,204,412'
          )
          echo "Đã nhận HTTP status sidecar checksum=${checksumResponse.status}, metadata=${metadataResponse.status}."

          sh 'mkdir -p verified-release'
          httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            httpMode: 'GET',
            ignoreSslErrors: false,
            outputFile: 'verified-release/widget-1.4.0.tgz.sha256',
            timeout: 30,
            url: env.REPOSITORY_SHA256_URL,
            validResponseCodes: '200'
          )
          httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            httpMode: 'GET',
            ignoreSslErrors: false,
            outputFile: 'verified-release/build-metadata.json',
            timeout: 30,
            url: env.REPOSITORY_METADATA_URL,
            validResponseCodes: '200'
          )
          sh '''#!/usr/bin/env sh
            set -eu
            cmp -s "$ARTIFACT_SHA256" verified-release/widget-1.4.0.tgz.sha256
            cmp -s "$ARTIFACT_METADATA" verified-release/build-metadata.json
            python3 - "$ARTIFACT_FILE" verified-release/widget-1.4.0.tgz.sha256 \
              verified-release/build-metadata.json <<'PY'
import hashlib
import json
from pathlib import Path
import sys

artifact, checksum_file, metadata_file = map(Path, sys.argv[1:])
expected = checksum_file.read_text(encoding='utf-8').split()[0]
actual = hashlib.sha256(artifact.read_bytes()).hexdigest()
metadata = json.loads(metadata_file.read_text(encoding='utf-8'))
assert expected == actual
assert metadata['subject'] == {
    'name': artifact.name,
    'version': '1.4.0',
    'sha256': expected,
}
PY
          '''

          def artifactResponse = httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            contentType: 'APPLICATION_OCTETSTREAM',
            customHeaders: [[maskValue: false, name: 'If-None-Match', value: '*']],
            httpMode: 'PUT',
            ignoreSslErrors: false,
            timeout: 30,
            uploadFile: env.ARTIFACT_FILE,
            url: env.REPOSITORY_ARTIFACT_URL,
            validResponseCodes: '200:201,204,412'
          )
          echo "Đã nhận HTTP status publication marker package=${artifactResponse.status}."
          httpRequest(
            authentication: 'artifact-release-publisher',
            consoleLogResponseBody: false,
            httpMode: 'GET',
            ignoreSslErrors: false,
            outputFile: 'verified-release/widget-1.4.0.tgz',
            timeout: 30,
            url: env.REPOSITORY_ARTIFACT_URL,
            validResponseCodes: '200'
          )
        }
        sh '''#!/usr/bin/env sh
          set -eu
          (cd verified-release && sha256sum -c widget-1.4.0.tgz.sha256)
          cmp -s "$ARTIFACT_SHA256" verified-release/widget-1.4.0.tgz.sha256
          cmp -s "$ARTIFACT_METADATA" verified-release/build-metadata.json
          python3 - verified-release/widget-1.4.0.tgz \
            verified-release/widget-1.4.0.tgz.sha256 \
            verified-release/build-metadata.json <<'PY'
import hashlib
import json
from pathlib import Path
import sys

artifact, checksum_file, metadata_file = map(Path, sys.argv[1:])
expected = checksum_file.read_text(encoding='utf-8').split()[0]
actual = hashlib.sha256(artifact.read_bytes()).hexdigest()
metadata = json.loads(metadata_file.read_text(encoding='utf-8'))
assert expected == actual
assert metadata['subject'] == {
    'name': artifact.name,
    'version': '1.4.0',
    'sha256': expected,
}
PY
        '''
        archiveArtifacts artifacts: 'dist/release/widget-1.4.0.tgz.sha256,dist/release/build-metadata.json,verified-release/widget-1.4.0.tgz.sha256,verified-release/build-metadata.json',
          allowEmptyArchive: false, fingerprint: true
      }
      post {
        cleanup { deleteDir() }
      }
    }
  }
}

authentication là credential ID, không phải token. consoleLogResponseBody: false giảm nguy cơ response nhạy cảm đi vào console. Cùng publisher identity trong mẫu chỉ cần PUT và GET trên ba path của release để read-after-write verification; nó không cần quyền xóa hay quản trị repository. If-None-Match: * làm 412 trở thành signal object đã tồn tại, không cho PUT ghi đè. Pipeline PUT checksum/metadata trước, GET/verify chúng, rồi PUT package là publication marker cuối cùng và GET lại đủ ba object để xác minh. Nếu sidecar fail, package marker chưa được tạo; sidecar còn lại là trạng thái chưa hoàn chỉnh mà consumer phải bỏ qua. Nếu package marker trả 412, Pipeline vẫn GET/verify toàn bộ ba object và chỉ tiếp tục khi bytes/subject/version/digest cùng khớp. Sidecar không có package marker chỉ được owner cleanup theo retention sau khi xác nhận marker vắng mặt; không upload đè để “sửa” trạng thái dở dang. Generic HTTP không tự có transaction/promote atomic: nếu repository không honor precondition hoặc không bảo đảm object chỉ visible sau PUT hoàn tất, dùng API promote/rename atomic do Nexus, Artifactory hoặc provider object storage hỗ trợ thay vì mẫu này. Với Maven/npm/OCI, thay stage publish bằng client/protocol tương ứng, giữ nguyên branch gate, identity least privilege, checksum/digest verification và evidence.

Idempotency và failure

Không bọc httpRequest trong retry mặc định. Nếu timeout xảy ra sau khi server nhận bytes, retry mù có thể tạo conflict hoặc overwrite tùy backend. Với semantics publication marker của mẫu, quy trình an toàn là:

  1. PUT checksum và metadata với If-None-Match: *; status 412 chỉ báo object đã có, chưa là thành công release;
  2. GET hai sidecar, đối chiếu SHA-256 và subject/version/digest với bytes local;
  3. chỉ sau bước 2, PUT package marker với cùng precondition, rồi GET package và xác minh đủ ba object;
  4. khi bất kỳ PUT trả 412, GET/verify toàn bộ identity; chỉ coi idempotent khi ba object cùng bytes và cùng identity;
  5. nếu sidecar tồn tại nhưng package marker vắng mặt, consumer bỏ qua; owner dọn theo retention sau khi kiểm marker vẫn vắng, không ghi đè nó.

PUT không tự mang nghĩa immutable hay atomic trên mọi server. Object storage có thể cần conditional request/versioning/object lock; Nexus/Artifactory cần policy chặn redeploy hoặc API promote; Maven/npm/OCI có rule format riêng. Không suy diễn một response HTTP xanh là policy release đã đúng.

Retention lifecycle và khôi phục

Retention phải tách candidate, release, evidence và cache. Mỗi loại có owner, quota, legal/compliance hold và restore path riêng.

Dữ liệuGợi ý lifecycleGuard bắt buộc
Candidate/RCTTL ngắn sau khi hết review windowKhông xóa candidate còn đang được promotion/job tham chiếu
Release immutableTheo support, rollback và compliance policyKhông overwrite hoặc xóa khi manifest/consumer còn dùng
Checksum, SBOM, provenance/attestationÍt nhất bằng release mà chúng mô tảGiữ link/version/digest có thể truy vết
Jenkins archive/reportNgắn hơn repository release, theo nhu cầu điều traQuota controller/Artifact Manager và ACL download
Build/dependency cacheTTL/quota theo trust tierKhông dùng làm nguồn phát hành hoặc dùng chung ghi với PR

Trước cleanup repository, tạo inventory artifact đang được environment manifest, release train, legal hold và consumer pin tham chiếu. Kiểm thử restore/download trong sandbox: fetch đúng version, verify checksum, đọc metadata và xác nhận ACL. Xóa package production không phải cách giảm quota khẩn cấp; dừng promotion, mở change record và để owner repository quyết định.

Lab local tái lập không cần repository

Lab tạo generic package, checksum và metadata dưới một directory mktemp. Thư mục repository/ bên trong fixture chỉ mô phỏng namespace và retention local; không có HTTP, Nexus, Artifactory, cloud bucket, Jenkins controller hay credential. Chạy toàn bộ block trong một Bash shell, không chạy bằng sudo.

Tạo và publish fixture

set -eu
umask 077

LAB_PARENT="$(cd "${TMPDIR:-/tmp}" && pwd -P)"
LAB_PREFIX='jenkins-artifact-repository-lab.'
LAB_ROOT="$(mktemp -d "${LAB_PARENT}/${LAB_PREFIX}XXXXXX")"
LAB_MARKER="${LAB_ROOT}/.lab-owned-marker"

case "$LAB_ROOT" in
  "$LAB_PARENT"/"$LAB_PREFIX"*) ;;
  *) printf '%s\n' 'Refuse: unexpected lab prefix.' >&2; exit 1 ;;
esac
test "$(dirname "$LAB_ROOT")" = "$LAB_PARENT"
printf '%s\n' 'jenkins-artifact-repository-lab-v1' > "$LAB_MARKER"

WORKSPACE="$LAB_ROOT/workspace"
REPOSITORY="$LAB_ROOT/repository"
RELEASE_DIR="$REPOSITORY/releases/acme/widget/1.4.0"
CANDIDATE_DIR="$REPOSITORY/candidates"
mkdir -p "$WORKSPACE/dist/release" "$RELEASE_DIR" "$CANDIDATE_DIR"

printf 'widget release built from synthetic local fixture\n' > "$WORKSPACE/dist/release/widget-1.4.0.tgz"
(
  cd "$WORKSPACE/dist/release"
  sha256sum widget-1.4.0.tgz > widget-1.4.0.tgz.sha256
)
LAB_SHA="$(awk '{print $1}' "$WORKSPACE/dist/release/widget-1.4.0.tgz.sha256")"
printf '{"subject":{"name":"widget-1.4.0.tgz","version":"1.4.0","sha256":"%s"},"source":"local-fixture"}\n' "$LAB_SHA" \
  > "$WORKSPACE/dist/release/build-metadata.json"

publish_immutable() {
  source_file="$1"
  destination_file="$2"
  if test -e "$destination_file"; then
    printf '%s\n' 'Refuse: immutable destination already exists.' >&2
    return 1
  fi
  install -m 0644 "$source_file" "$destination_file"
}

publish_immutable \
  "$WORKSPACE/dist/release/widget-1.4.0.tgz" \
  "$RELEASE_DIR/widget-1.4.0.tgz"
install -m 0644 "$WORKSPACE/dist/release/widget-1.4.0.tgz.sha256" \
  "$RELEASE_DIR/widget-1.4.0.tgz.sha256"
install -m 0644 "$WORKSPACE/dist/release/build-metadata.json" \
  "$RELEASE_DIR/build-metadata.json"

# Candidate này mô phỏng artifact hết TTL; release không nằm dưới CANDIDATE_DIR.
printf 'expired candidate\n' > "$CANDIDATE_DIR/widget-1.4.1-rc.2.tgz"
touch -t 202001010000 "$CANDIDATE_DIR/widget-1.4.1-rc.2.tgz"
find "$CANDIDATE_DIR" -type f -name '*.tgz' -mtime +7 -delete

printf 'LAB_ROOT=%s\n' "$LAB_ROOT"
printf 'immutable local publish: PASS\n'

Kiểm tra và cleanup có guard

Checksum file dùng path tuyệt đối của workspace lúc được tạo. Khi kiểm package ở repository, tính lại SHA-256 và so sánh digest thay vì gọi sha256sum -c từ directory khác.

set -eu
: "${LAB_ROOT:?Run the fixture block in this shell first}"
: "${LAB_MARKER:?Run the fixture block in this shell first}"
case "$LAB_ROOT" in
  "$LAB_PARENT"/"$LAB_PREFIX"*) ;;
  *) printf '%s\n' 'Refuse: invalid lab prefix.' >&2; exit 1 ;;
esac
test "$(dirname "$LAB_ROOT")" = "$LAB_PARENT"
test "$(cat "$LAB_MARKER")" = 'jenkins-artifact-repository-lab-v1'

SOURCE_SHA="$(awk '{print $1}' "$WORKSPACE/dist/release/widget-1.4.0.tgz.sha256")"
REPOSITORY_SHA="$(sha256sum "$RELEASE_DIR/widget-1.4.0.tgz" | awk '{print $1}')"
test "$SOURCE_SHA" = "$REPOSITORY_SHA"
python3 - "$RELEASE_DIR/build-metadata.json" "$REPOSITORY_SHA" <<'PY'
import json
from pathlib import Path
import sys
metadata = json.loads(Path(sys.argv[1]).read_text(encoding='utf-8'))
assert metadata['subject'] == {
    'name': 'widget-1.4.0.tgz',
    'version': '1.4.0',
    'sha256': sys.argv[2],
}
PY
test ! -e "$CANDIDATE_DIR/widget-1.4.1-rc.2.tgz"
printf 'checksum and retention simulation: PASS\n'

cleanup_lab() {
  test -n "${LAB_ROOT:-}"
  test -n "${LAB_PARENT:-}"
  case "$LAB_ROOT" in
    "$LAB_PARENT"/"$LAB_PREFIX"*) ;;
    *) printf '%s\n' 'Refuse: unexpected cleanup path.' >&2; return 1 ;;
  esac
  test "$(dirname "$LAB_ROOT")" = "$LAB_PARENT"
  test -f "$LAB_MARKER"
  test "$(cat "$LAB_MARKER")" = 'jenkins-artifact-repository-lab-v1'
  cd / || return 1
  rm -rf -- "$LAB_ROOT"
}

cleanup_lab
test ! -e "$LAB_ROOT"
printf 'guarded cleanup: PASS\n'

Expected output có immutable local publish: PASS, checksum and retention simulation: PASS và guarded cleanup: PASS. Gọi publish_immutable lần hai với cùng destination phải fail; đó là mô phỏng policy release không ghi đè.

Giới hạn của lab

Lab chỉ chứng minh shell tạo bytes, checksum, guard và lifecycle local. Nó không kiểm tra Jenkins Declarative syntax, HTTP Request Plugin, credential binding, TLS/CA, Nexus, Artifactory, S3-compatible API, object lock, repository audit, permission hay network. Các điều kiện runtime này cần controller/agent và repository sandbox riêng; không dùng token, namespace hoặc bucket production để xác minh ví dụ.

Troubleshooting

Triệu chứngEvidence cần xemHướng xử lý an toàn
Upload trả 401 hoặc 403Credential ID, folder/job scope, repository path permission, audit timestampSửa đúng scope/quyền path; không in token hoặc cấp admin.
TLS/CA failHostname, CA chain, proxy policy và agent truststoreCài CA qua image/config đã review; không tắt TLS verification.
409 hoặc package đã tồn tạiVersion/path, size, checksum và repository metadataNếu bytes giống nhau, ghi idempotency evidence; nếu khác, fail và điều tra.
Timeout uploadRequest ID, upload size, server audit và checksumĐọc metadata trước retry; không retry mù thao tác không idempotent.
Consumer nhận bytes saiVersion/digest, SHA-256, mirror/cache metadataDownload lại từ version bất biến, verify checksum và dọn cache theo policy.
Jenkins disk đầyKích thước archive/stash, retention build và Artifact Manager backendThu hẹp archive, đặt quota/lifecycle; không xóa release repository để sửa disk controller.
Candidate bị dọn khi còn dùngPromotion record, manifest reference, lifecycle rule và ownerDừng cleanup, restore theo version/checksum, sửa guard và retention policy.
PR có thể publishBranch policy, when, credential scope, agent/cache isolationGỡ credential khỏi lane PR, tách pool và review Multibranch trust policy.

Checklist tự kiểm tra

  • Đã chọn đúng format/giao thức; generic HTTP không bị dùng thay Maven, npm hoặc OCI client.
  • Publisher chỉ ghi namespace cần thiết; consumer read-only; credential có owner, expiry và rotation plan.
  • TLS hostname/CA, proxy/egress, repository audit và quota đã được kiểm tra trên sandbox.
  • Jenkinsfile không chứa token/password, URL query có secret, Groovy interpolation nhạy cảm, set -x hay archive workspace rộng.
  • Pull request/fork không nhận credential publish, pool release, cache release ghi được hoặc quyền promotion.
  • Release version/digest bất biến; server chặn overwrite; SHA-256, SBOM và metadata có thể truy vết.
  • Promotion/rollback dùng cùng bytes/version/digest; không build lại hoặc dùng alias/tag di động.
  • Retention tách candidate, release, evidence và cache; không xóa artifact còn được manifest, rollback hoặc hold tham chiếu.
  • Upload timeout có runbook metadata/checksum/idempotency trước retry.
  • Lab local đã in ba dòng pass; Jenkins/repository runtime chỉ được ghi nhận sau sandbox riêng.

Nguồn chính thức và đọc tiếp

On this page