مستندات Infynexراهنماهای پلتفرم، DevOps و انتشار
مستندات / عملیات انتشار / تغییر Tag و Digest ایمیج
عملیات انتشار

تغییر Tag و Digest ایمیج

تفاوت Tag/Digest، جریان Promote/Mirror و اینکه چه چیزی در Helm تغییر می‌کند.

تغییر Tag و Digest ایمیج در Helm و رجیستری Infynex

وقتی Tag ساخته یا Promote می‌شود، دقیقاً چه چیزی در آدرس ایمیج‌های چارت‌های Helm تغییر می‌کند؟ تفاوت Tag و Digest چیست، کدام اسکریپت چه کاری می‌کند و کنترل در چه سطحی انجام می‌شود؟

مثال: v1.7.0-rc.2 ← v1.7.0همراه با دیاگرام و دستورهای بررسیمکمل «راهنمای انتشار نسخه»
با تغییر این دو مقدار، مثال‌های سند به‌روز می‌شوند.

۱. خلاصهٔ اجرایی

پاسخ کوتاه بله. هنگام Promote (مثلاً v1.7.0-rc.2 ← v1.7.0) Tag داخل فایل‌های چارت Helm عوض می‌شود، اما Digest (@sha256:…) تغییر نمی‌کند و هیچ Build دوباره‌ای انجام نمی‌شود. Digest فقط در لحظهٔ Build عوض می‌شود.
Tagبرچسبی قابل‌خواندن برای انسان؛ در Promote عوض می‌شود
Digestاثر انگشت محتوا؛ در Promote و Mirror ثابت می‌ماند
Rebuildدر Promote و Mirror وجود ندارد

نتیجهٔ عملی: بایت‌هایی که در تست RC E2E را گذرانده‌اند، عیناً همان بایت‌هایی‌اند که با نام v1.7.0 منتشر می‌شوند.

دو رجیستری OCI
  • git.infynex.ir/infynex-io — رجیستری CI/داخلی: خروجی Buildِ PR و main.
  • ghcr.io/infynex-io/infynex — رجیستری Release: RC، Stable و Nightly برای توزیع.
کپی بین این دو همیشه بر پایهٔ Digest انجام می‌شود، نه Retag ساده؛ یعنی بیت‌به‌بیت یکسان است.

۲. Tag و Digest چیستند؟

یک مرجع کامل ایمیج (Image Ref) معمولاً سه بخش دارد:

ghcr.io/infynex-io/infynex/infynex-api:v1.7.0@sha256:9f3c…a1
مخزن (Registry + Repository): ایمیج کجاست Tag: برچسب/ورژن قابل‌خواندن؛ می‌تواند عوض شود یا جابه‌جا شود Digest: هش محتوا؛ تغییرناپذیر
تشبیه Tag مثل برچسب روی یک جعبه است: می‌توان آن را کند و روی جعبهٔ دیگری چسباند. Digest مثل اثر انگشت محتوای جعبه است: اگر حتی یک بایت فرق کند، عوض می‌شود. به همین دلیل چارت‌ها هر دو را کنار هم نگه می‌دارند: Tag برای خواندن، Digest برای اطمینان.

وقتی Tag و Digest هر دو وجود دارند، Pull بر اساس Digest انجام می‌شود و Tag فقط یک توضیح انسانی است. پس تغییر Tag روی «همان Digest» به‌معنای «همان ایمیج با نام جدید» است.

۳. Tag و Digest چه زمانی تغییر می‌کنند؟

مقطعTagDigestنکته
Build روی PRpr-<N>-<sha>جدیدمنحصر به Commit همان PR؛ از Tag شناور مشترک استفاده نمی‌شود تا Pushهای هم‌زمان تداخل نکنند.
شاخهٔ mainmain (شناور)جدیدbuild-main.yaml با هر Push؛ در رجیستری CI.
Tag پیش‌انتشارvX.Y.Z-rc.Nجدیدtags.yaml آن را Build می‌کند و Digestها را در شاخهٔ مرحله‌ای release-X.Y.Z-rc.N ثابت می‌کند.
Promote به StablevX.Y.Zثابتهمان Digest RC؛ فقط بازنویسی/Retag، بدون Build.
Nightly Mirror0.0.0-nightly.… و nightlyثابتکپی بیت‌به‌بیت از رجیستری CI به Release.
قاعدهٔ طلایی Digest فقط در Build تغییر می‌کند. در Promote و Mirror فقط Tag (و در Mirror رجیستری مقصد) عوض می‌شود.

نکته: شاخهٔ release-X.Y یک شاخهٔ Git است نه Tag ایمیج؛ در فایل اولیه Build آن به‌عنوان یک ردیف جدا آمده بود، ولی در مستند انتشار اشاره‌ای به Tag ایمیج مستقل برای آن نیست و حذف شده است.

۴. جریان Build، Promote و Mirror

flowchart LR
  PR["PR build
tag pr-N-sha
NEW digest"] --> MAIN["main build
tag main
NEW digest"] MAIN --> RC["cut-prerelease + tags.yaml
tag v1.7.0-rc.N
NEW digest"] RC --> E2E{"rc-e2e green?"} E2E -- yes --> PRM["promote-rc.yaml
rewrite tags in chart files
SAME digest"] PRM --> FIN["Finalize
retag by digest to v1.7.0
SAME digest"] FIN --> LATEST[":latest only if newest stable"] MAIN -. nightly .-> MIR["nightly-mirror.sh
copy by digest CI to release
SAME digest"]
فقط سه گره اول Digest جدید می‌سازند؛ از Promote به بعد Digest ثابت است
sequenceDiagram
    autonumber
    participant CI as CI registry
    participant RC as release registry (rc)
    participant GIT as release-1.7.0 branch
    participant FIN as Finalize
    participant ST as release registry (stable)
    CI->>RC: tags.yaml builds v1.7.0-rc.2 (digest D1)
    RC-->>GIT: Prepare release commit pins digests D1
    GIT->>GIT: promote rewrites ":v1.7.0-rc.2@D1" to ":v1.7.0@D1"
    GIT->>GIT: second commit pins packages artifact digest
    GIT->>FIN: promote PR merged
    FIN->>RC: verify candidate and container digests
    FIN->>ST: retag by digest: v1.7.0 points to D1
Digest ‏D1 از ساخت RC تا Stable دست‌نخورده می‌ماند

۵. در فایل‌های Helm دقیقاً چه عوض می‌شود؟

مثال قبل و بعد از Promote

پیش از Promote (RC)پس از Promote (Stable)
infynexAPI.image: ghcr.io/infynex-io/infynex/infynex-api:v1.7.0-rc.2@sha256:9f3c…a1infynexAPI.image: ghcr.io/infynex-io/infynex/infynex-api:v1.7.0@sha256:9f3c…a1
cilium.image.tag: v1.7.0-rc.2@sha256:1234…efcilium.image.tag: v1.7.0@sha256:1234…ef
# شکل Diff در Promote PR
- image: infynex-api:v1.7.0-rc.2@sha256:9f3c…a1
+ image: infynex-api:v1.7.0@sha256:9f3c…a1
آنچه ثابت است
  • بخش @sha256:… در هر دو حالت یکسان است.
  • فقط بخش Tag (:v1.7.0-rc.2 ← :v1.7.0) عوض می‌شود.
  • اگر میزبان (Host) رجیستری جدا نگه‌داری شده باشد، Host و Repository هم با هم بازنویسی می‌شوند (طبق فایل اولیه).
دو Commit روی شاخهٔ Promote نخست، درخت بازنویسی‌شده (Tag) Commit می‌شود؛ دوم، Digest آرتیفکت packages در Installer ثابت می‌شود. به همین دلیل Digest کانتینرها ثابت می‌ماند ولی یک مقدار تازه هم وارد می‌شود (بخش ۷).

۶. مرجع‌های ایمیج کجا ذخیره می‌شوند؟

Commit ‏«Prepare release vX.Y.Z» فقط Digest و Tag Image را عوض می‌کند. فایل‌های لمس‌شده:

مسیرمحتوا
packages/core/{installer,platform,testing}/values.yamlمرجع‌های هستهٔ پلتفرم، Installer و آرتیفکت packages
packages/system/*/values.yamlاجزای سیستمی (API، داشبورد، kamaji، linstor، kubevirt-csi-node و …)
packages/apps/kubernetes/images/*.tagفایل ساده با یک مرجع در هر فایل (مثل kubevirt-csi-driver)؛ از Template با .Files.Get خوانده می‌شود
packages/system/dashboard/templates/configmap.yamlمقدار $tenantText

فایل اولیه چهار «شکل» ذخیره‌سازی را برمی‌شمرد و یک استثنا برای فایل‌های Vendor‌شده از بالادست (مثل DaemonSet ‏multus):

شکلتوضیح
۱values.yaml: رشتهٔ کامل، یا نقشهٔ جدا (repository/tag/digest)، یا مقدار سراسری Chart، یا آرتیفکت OCI
۲images/*.tag: فایل ساده با یک مرجع
۳فایل‌های اضافه (IMAGE_REF_EXTRA_FILES) مثل system/multus/templates/multus-daemonset-thick.yml
راستی‌آزمایی این بخش فهرست فایل‌ها از مستند انتشار گرفته شده و قابل اتکاست. شمارهٔ دقیق «شکل‌ها»، نام IMAGE_REF_EXTRA_FILES و توابع image_ref_files() و collect_image_refs() از فایل اولیهٔ شماست؛ من نتوانستم مستند docs/agents/image-refs.md را بخوانم. پیش از انتشار این سند، آن‌ها را با فایل واقعی مخزن تطبیق دهید.

چیزهای عجیب‌نما اما عادی در Diff

  • پکیج بدون تغییر: وقتی Cache کامل بوده و Base Imageها جابه‌جا نشده‌اند، Digest عیناً همان قبلی است. «بدون Diff» یعنی «Build نشده» نیست؛ اجرای گام Build را بررسی کنید.
  • تغییر ناگهانی Tagهای ubuntu-container-disk-*: منبع بالادستی یک هدف متحرک است.
  • تغییر رجیستری: اگر ایمیجی از یک رجیستری به دیگری منتقل شده، بخش رجیستریِ مرجع عوض می‌شود؛ این یک انتقال عمدی است.
  • targetVersion را PR انتشار تغییر نمی‌دهد: این عدد قبلاً در یک Commit قابلیت بالا رفته؛ PR انتشار فقط Digest ایمیج Migrationها را ثابت می‌کند.

۷. استثنا: آرتیفکت packages و «ارجاع به خود»

علاوه بر ایمیج‌های کانتینر، پلتفرم یک آرتیفکت OCI به نام packages (درخت packages/) منتشر می‌کند. Digest این آرتیفکت در Promote جدید است، چون درخت بازنویسی شده است:

  1. Promote درخت را بازنویسی می‌کند (Tag ← 1.7.0) و زیر یک Tag موقت promotion-* منتشر می‌کند.
  2. Commit دوم Digest تغییرناپذیر همین Candidate را در Installer ثابت می‌کند.
  3. آرتیفکت نمی‌تواند Digest خودش را در خودش داشته باشد؛ این «ارجاع به خود» غیرممکن است و هنگام تأیید نادیده گرفته می‌شود.
مورددر Promote
Digest ایمیج‌های کانتینرثابت همان RC
Digest آرتیفکت packagesجدید (Candidate)، بعد از Merge با Digest به v1.7.0 Retag می‌شود
تأیید پیش از Tag پایدار Finalize پیش از ساخت هر Tag پایدار آرتیفکت را با Digest می‌کشد، با درخت Merge‌شده تطبیق می‌دهد، مطمئن می‌شود هیچ Tag مربوط به rc باقی نمانده و مجموعهٔ repo@digest کانتینرها با آرتیفکت RC اصلی برابر است. هر ناهماهنگی، Finalize را قبل از ساخت هر نام پایدار متوقف می‌کند.

۸. اسکریپت‌ها و workflowها

فایلنقشمنبع
hack/lib/image-refs.shتنها فهرست‌کنندهٔ فایل‌های دارای مرجع ایمیج؛ مشترک بین Promote، Retag و Mirrorمستند انتشار
hack/promote-rewrite-tags.shبازنویسی Tag در درخت packages/ هنگام Promoteفایل اولیه
hack/promote-retag.shRetag آرتیفکت و ایمیج‌های RC با Digest در Finalize، بدون Buildمستند انتشار
hack/verify-promoted-packages.shتأیید تطابق Candidate با درخت Merge‌شده و Digestهای RC پیش از ساخت Tag پایدارمستند انتشار
hack/nightly-mirror.shکپی Digest به Digest از رجیستری CI به رجیستری Releaseمستند انتشار
.github/workflows/promote-rc.yamlPromote: بازنویسی، Candidate، PRمستند انتشار
.github/workflows/pull-requests-release.yamlFinalize: تأیید، Tag، Retagمستند انتشار
.github/workflows/nightly.yamlMirror و E2E روزانهمستند انتشار
hack/build-matrix.sh، hack/overlay-main-images.shماتریس Build و روکش Imageهای main (ابزار Packaging، نه منطق کنترلری)فایل اولیه

ردیف‌های «فایل اولیه» در این گفتگو راستی‌آزمایی نشده‌اند. فایل docs/alm-versioning-release-guide.md که در فایل اولیه آمده بود هم همین‌طور؛ در مستند انتشار چنین نامی نیست و از جدول حذف شد.

۹. کنترل در سطح چارت است یا کنترلرهای کلاستر؟

پاسخ عمدتاً در سطح چارت Helm و ابزارهای CI/Release است، نه در کنترلرهای داخل کلاستر.
  • کنترلرهای کلاستر فقط مصرف‌کننده‌اند: چارت رندر می‌شود و Deployment/StatefulSet/Pod با image: repo:tag@sha256:… ساخته می‌شود. کنترلر هیچ‌چیز را به values.yaml یا فایل چارت بازنمی‌نویسد.
  • بازنویسی Tag، Retag، Mirror و تأیید Digest همه با اسکریپت‌های Shell و workflowهای CI انجام می‌شوند.
flowchart LR
  CI["CI scripts and workflows"] -->|rewrite tag| CH["Helm chart files"]
  CI -->|retag by digest| REG["OCI registry"]
  CH -->|helm template| MAN["rendered manifests"]
  MAN -->|deploy| K8S["Kubernetes cluster"]
  K8S --> CTL["controllers (read only)"]
  CTL -. never write .-> CH
جهت جریان: از CI به چارت و رجیستری، هرگز از کنترلر به چارت

۱۰. Nightly Mirror

Nightly یک کپی قابل‌نصب از main روی رجیستری Release است؛ نه Build دوباره و نه Release. Build هر Push روی main قبلاً در رجیستری CI ساخته شده؛ nightly.yaml آن را ارتقا می‌دهد و نصب‌پذیری‌اش را اثبات می‌کند:

  1. mirror: Commit دقیقی که packages:main از آن ساخته شده پیدا می‌شود و تمام ایمیج‌های اجزای پلتفرم با Digest کپی می‌شوند. آرتیفکت packages و Chart نصب‌کننده با Tag زیر منتشر می‌شوند: 0.0.0-nightly.<YYYYMMDD>.<run-id>.<attempt> به‌علاوهٔ Tag شناور nightly.
  2. build-disk: دیسک Talos از Imager بالادستی ساخته و منتشر می‌شود.
  3. e2e: مجموعهٔ کامل اپلیکیشن‌ها روی نتیجه اجرا می‌شود.

برای Nightly هیچ GitHub Release و Tag ماژول Go ساخته نمی‌شود، و retention.yaml نسخه‌های قدیمی را پاک می‌کند (۱۴ نسخهٔ اخیر هر بسته می‌ماند).

۱۱. خودتان چطور بررسی کنید؟

با crane می‌توانید ثابت بودن Digest را عملاً ببینید:

# Digest مربوط به هر دو Tag باید یکسان باشد
crane digest ghcr.io/infynex-io/infynex/infynex-api:v1.7.0-rc.2
crane digest ghcr.io/infynex-io/infynex/infynex-api:v1.7.0

# مرجع با Digest باید Pull شود (مثلاً هنگام بازبینی Promote PR)
crane manifest ghcr.io/infynex-io/infynex/infynex-api@sha256:9f3c…a1 | head

# فقط Tag عوض شده، Digest نه: Diff شاخهٔ Promote
git diff release-1.7...release-1.7.0 -- 'packages/**/values.yaml' | grep -E '^[+-].*image'
چک‌لیست بازبین
  • ☐ در Diff فقط Tag عوض شده و بخش @sha256:… هر خط یکسان است.
  • ☐ crane manifest …@digest برای هر Digest تغییرکرده جواب می‌دهد.
  • ☐ Verify release packages candidate در PR سبز است.
  • ☐ هیچ Tag ‏-rc. در درخت Merge‌شده باقی نمانده.

۱۲. پرسش‌های پرتکرار

پرسشپاسخ
چرا Stable دوباره Build نمی‌شود؟چون باید همان چیزی منتشر شود که E2E را گذرانده؛ Build دوباره ممکن است بایت‌های دیگری بدهد.
اگر بعد از ساخت RC چیزی در main عوض شود؟در Stable نیست. rc بعدی باید از release-X.Y و با Cherry-pick ساخته شود.
آیا Tag ‏latest همیشه جابه‌جا می‌شود؟نه؛ فقط وقتی نسخه جدیدترین Stable منتشرشده باشد.
اگر Digest در Finalize با RC نخواند چه می‌شود؟Finalize پیش از ساخت هر Tag پایدار متوقف می‌شود و هیچ نام پایداری ساخته نمی‌شود.
Promote رهاشده چه می‌گذارد؟فقط یک Tag موقت promotion-*؛ نام پایدار ساخته نمی‌شود و Retention آن را بعد از ۳۰ روز پاک می‌کند.
Tag را می‌توان جابه‌جا کرد؟خیر. Tagهای v* Write-once‌اند؛ اصلاح یعنی نسخهٔ Patch بعدی.

برای جزئیات کامل مراحل Promote و Finalize به «راهنمای انتشار نسخه در پلتفرم Infynex» مراجعه کنید.