Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package werf for openSUSE:Factory checked in at 2026-11-13 18:34:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/werf (Old) and /work/SRC/openSUSE:Factory/.werf.new.1251 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "werf" Fri Nov 13 18:34:56 2026 rev:89 rq:1397245 version:2.84.0 Changes: -------- --- /work/SRC/openSUSE:Factory/werf/werf.changes 2026-10-29 16:10:36.224789298 +0100 +++ /work/SRC/openSUSE:Factory/.werf.new.1251/werf.changes 2026-11-13 18:38:55.515426790 +0100 @@ -1,0 +2,12 @@ +Fri Nov 13 05:32:47 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 2.84.0: + * Features + - build: publish cached images with require-built-images + (#8096) (bf5c561) +- Update to version 2.83.1: + * Bug Fixes + - build: avoid panic on cache-repo errors without final-repo + (#8088) (acdca35) + +------------------------------------------------------------------- Old: ---- werf-2.83.0.obscpio New: ---- werf-2.84.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ werf.spec ++++++ --- /var/tmp/diff_new_pack.poTGC7/_old 2026-11-13 18:39:00.813650955 +0100 +++ /var/tmp/diff_new_pack.poTGC7/_new 2026-11-13 18:39:00.817651124 +0100 @@ -17,7 +17,7 @@ Name: werf -Version: 2.83.0 +Version: 2.84.0 Release: 0 Summary: CLI for the Werf CI/CD system License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.poTGC7/_old 2026-11-13 18:39:00.851652562 +0100 +++ /var/tmp/diff_new_pack.poTGC7/_new 2026-11-13 18:39:00.855652732 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/werf/werf.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v2.83.0</param> + <param name="revision">refs/tags/v2.84.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.poTGC7/_old 2026-11-13 18:39:00.880653789 +0100 +++ /var/tmp/diff_new_pack.poTGC7/_new 2026-11-13 18:39:00.884653959 +0100 @@ -3,6 +3,6 @@ <param name="url">https://github.com/werf/werf</param> <param name="changesrevision">508fb9aee29baca228674ba7a3d2d4fa1f5844d5</param></service><service name="tar_scm"> <param name="url">https://github.com/werf/werf.git</param> - <param name="changesrevision">393e5d5e9fe4bea56b226174e3d4397bc78ce602</param></service></servicedata> + <param name="changesrevision">4ca847c4b39ff5174f9174b41613133f4dfdcd62</param></service></servicedata> (No newline at EOF) ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/werf/vendor.tar.gz /work/SRC/openSUSE:Factory/.werf.new.1251/vendor.tar.gz differ: char 142, line 2 ++++++ werf-2.83.0.obscpio -> werf-2.84.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/.release-please/2/manifest.json new/werf-2.84.0/.release-please/2/manifest.json --- old/werf-2.83.0/.release-please/2/manifest.json 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/.release-please/2/manifest.json 2026-10-20 17:16:12.000000000 +0200 @@ -1,3 +1,3 @@ { - ".": "2.83.0" + ".": "2.84.0" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/CHANGELOG.md new/werf-2.84.0/CHANGELOG.md --- old/werf-2.83.0/CHANGELOG.md 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/CHANGELOG.md 2026-10-20 17:16:12.000000000 +0200 @@ -1,5 +1,19 @@ # Changelog +## [2.84.0](https://github.com/werf/werf/compare/v2.83.1...v2.84.0) (2026-10-20) + + +### Features + +* **build:** publish cached images with require-built-images ([#8096](https://github.com/werf/werf/issues/8096)) ([bf5c561](https://github.com/werf/werf/commit/bf5c56112b1759b886690199ef161e876be94dc5)) + +## [2.83.1](https://github.com/werf/werf/compare/v2.83.0...v2.83.1) (2026-10-19) + + +### Bug Fixes + +* **build:** avoid panic on cache-repo errors without final-repo ([#8088](https://github.com/werf/werf/issues/8088)) ([acdca35](https://github.com/werf/werf/commit/acdca350932073444c4d385c3325de6bfc390b59)) + ## [2.83.0](https://github.com/werf/werf/compare/v2.82.0...v2.83.0) (2026-10-15) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/build/main.go new/werf-2.84.0/cmd/werf/build/main.go --- old/werf-2.83.0/cmd/werf/build/main.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/cmd/werf/build/main.go 2026-10-20 17:16:12.000000000 +0200 @@ -44,6 +44,9 @@ }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() + if common.GetRequireBuiltImages(&commonCmdData) && common.GetCheckBuiltImages(&commonCmdData) { + return fmt.Errorf("--require-built-images cannot be used with --check-built-images") + } defer global_warnings.PrintGlobalWarnings(ctx) @@ -104,6 +107,8 @@ common.SetupParallelOptions(&commonCmdData, cmd, common.DefaultBuildParallelTasksLimit) common.SetupCheckBuiltImages(&commonCmdData, cmd) + common.SetupRequireBuiltImages(&commonCmdData, cmd) + cmd.Flags().Lookup("require-built-images").Usage = "Publish previously built images and custom tags without running build instructions. Reuse stages from primary and secondary repos; fail if a required stage is missing. Incompatible with --check-built-images (default $WERF_REQUIRE_BUILT_IMAGES)" common.SetupFollow(&commonCmdData, cmd) common.SetupDisableAutoHostCleanup(&commonCmdData, cmd) @@ -217,6 +222,8 @@ return err } + buildOptions.RequireBuiltImages = common.GetRequireBuiltImages(&commonCmdData) + conveyorOptions, err := common.GetConveyorOptionsWithParallel(ctx, &commonCmdData, imagesToProcess, buildOptions) if err != nil { return err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/build/require_built_images_test.go new/werf-2.84.0/cmd/werf/build/require_built_images_test.go --- old/werf-2.83.0/cmd/werf/build/require_built_images_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.84.0/cmd/werf/build/require_built_images_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -0,0 +1,65 @@ +package build + +import ( + "io" + "os" + "strings" + "testing" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/werf/werf/v2/cmd/werf/common" +) + +func TestBuildCommand(t *testing.T) { + gomega.RegisterFailHandler(ginkgo.Fail) + ginkgo.RunSpecs(t, "Build Command Suite") +} + +var _ = ginkgo.Describe("Require built images flags", func() { + ginkgo.BeforeEach(func() { + for _, entry := range os.Environ() { + key, _, _ := strings.Cut(entry, "=") + if strings.HasPrefix(key, "WERF_") { + ginkgo.GinkgoT().Setenv(key, "") + } + } + }) + + ginkgo.DescribeTable("rejects invalid mode combinations before initializing the build", + func(ctx ginkgo.SpecContext, args []string, env map[string]string, expected string) { + for key, value := range env { + ginkgo.GinkgoT().Setenv(key, value) + } + cmd := NewCmd(ctx) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + cmd.SetArgs(append([]string{}, args...)) + gomega.Expect(cmd.ExecuteContext(ctx)).To(gomega.MatchError(gomega.ContainSubstring(expected))) + }, + ginkgo.Entry("check flag", []string{"--require-built-images", "--check-built-images"}, nil, "cannot be used with --check-built-images"), + ginkgo.Entry("short flag", []string{"-Z", "--check-built-images"}, nil, "cannot be used with --check-built-images"), + ginkgo.Entry("check environment", []string{"--require-built-images"}, map[string]string{"WERF_CHECK_BUILT_IMAGES": "true"}, "cannot be used with --check-built-images"), + ginkgo.Entry("require environment", []string{"--check-built-images"}, map[string]string{"WERF_REQUIRE_BUILT_IMAGES": "true"}, "cannot be used with --check-built-images"), + ginkgo.Entry("both environments", nil, map[string]string{"WERF_REQUIRE_BUILT_IMAGES": "true", "WERF_CHECK_BUILT_IMAGES": "true"}, "cannot be used with --check-built-images"), + ) + ginkgo.DescribeTable("selects independent check and require modes", func(ctx ginkgo.SpecContext, args []string, env map[string]string, require, check bool) { + for key, value := range env { + ginkgo.GinkgoT().Setenv(key, value) + } + cmd := NewCmd(ctx) + gomega.Expect(cmd.ParseFlags(args)).To(gomega.Succeed()) + gomega.Expect(common.GetRequireBuiltImages(&commonCmdData)).To(gomega.Equal(require)) + gomega.Expect(common.GetCheckBuiltImages(&commonCmdData)).To(gomega.Equal(check)) + }, + ginkgo.Entry("ordinary build", nil, nil, false, false), + ginkgo.Entry("require flag", []string{"--require-built-images"}, nil, true, false), + ginkgo.Entry("short flag", []string{"-Z"}, nil, true, false), + ginkgo.Entry("require environment", nil, map[string]string{"WERF_REQUIRE_BUILT_IMAGES": "true"}, true, false), + ginkgo.Entry("check flag", []string{"--check-built-images"}, nil, false, true), + ginkgo.Entry("check environment", nil, map[string]string{"WERF_CHECK_BUILT_IMAGES": "true"}, false, true), + ginkgo.Entry("explicit require override", []string{"--require-built-images=false"}, map[string]string{"WERF_REQUIRE_BUILT_IMAGES": "true"}, false, false), + ginkgo.Entry("explicit check override", []string{"--check-built-images=false", "-Z"}, map[string]string{"WERF_CHECK_BUILT_IMAGES": "true"}, true, false), + ) +}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/common/cmd_data.go new/werf-2.84.0/cmd/werf/common/cmd_data.go --- old/werf-2.83.0/cmd/werf/common/cmd_data.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/cmd/werf/common/cmd_data.go 2026-10-20 17:16:12.000000000 +0200 @@ -47,10 +47,9 @@ SecondaryStagesStorage *[]string CacheStagesStorage *[]string - CheckBuiltImages *bool - LegacyCheckBuiltImages *bool // TODO(major): remove - RequireBuiltImages *bool - StubTags *bool + CheckBuiltImages *bool + RequireBuiltImages *bool + StubTags *bool AddCustomTag *[]string UseCustomTag *string diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/common/common.go new/werf-2.84.0/cmd/werf/common/common.go --- old/werf-2.83.0/cmd/werf/common/common.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/cmd/werf/common/common.go 2026-10-20 17:16:12.000000000 +0200 @@ -853,12 +853,6 @@ func SetupCheckBuiltImages(cmdData *CmdData, cmd *cobra.Command) { cmdData.CheckBuiltImages = new(bool) cmd.Flags().BoolVarP(cmdData.CheckBuiltImages, "check-built-images", "", util.GetBoolEnvironmentDefaultFalse("WERF_CHECK_BUILT_IMAGES"), "Check that all used images are previously built and exist in repo. Exits with error if needed images are not cached and so require to run build instructions (default $WERF_CHECK_BUILT_IMAGES)") - - cmdData.LegacyCheckBuiltImages = new(bool) - cmd.Flags().BoolVarP(cmdData.LegacyCheckBuiltImages, "require-built-images", "Z", false, "Check that all used images are previously built and exist in repo. Exits with error if needed images are not cached and so require to run build instructions") - if err := cmd.Flags().MarkHidden("require-built-images"); err != nil { - panic(err) - } } func SetupStubTags(cmdData *CmdData, cmd *cobra.Command) { @@ -1453,7 +1447,7 @@ } func GetCheckBuiltImages(cmdData *CmdData) bool { - return option.PtrValueOrDefault(cmdData.CheckBuiltImages, false) || option.PtrValueOrDefault(cmdData.LegacyCheckBuiltImages, false) + return option.PtrValueOrDefault(cmdData.CheckBuiltImages, false) } func isImagesReadOnly(cmdData *CmdData) bool { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/common/host_cleanup_test.go new/werf-2.84.0/cmd/werf/common/host_cleanup_test.go --- old/werf-2.83.0/cmd/werf/common/host_cleanup_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/cmd/werf/common/host_cleanup_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -32,8 +32,6 @@ gomega.Expect(skipAutoHostCleanup(cmdData)).To(gomega.BeTrue()) }, ginkgo.Entry("check flag", "--check-built-images"), - ginkgo.Entry("legacy flag", "--require-built-images"), - ginkgo.Entry("short flag", "-Z"), ginkgo.Entry("environment", ""), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/cmd/werf/common/storage_readonly_test.go new/werf-2.84.0/cmd/werf/common/storage_readonly_test.go --- old/werf-2.83.0/cmd/werf/common/storage_readonly_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/cmd/werf/common/storage_readonly_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -36,8 +36,6 @@ gomega.Expect(recorder.recordedWrites()).To(gomega.BeEmpty()) }, ginkgo.Entry("check flag", "--check-built-images"), - ginkgo.Entry("legacy flag", "--require-built-images"), - ginkgo.Entry("short flag", "-Z"), ginkgo.Entry("environment", ""), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/docs/_includes/reference/cli/werf_build.md new/werf-2.84.0/docs/_includes/reference/cli/werf_build.md --- old/werf-2.83.0/docs/_includes/reference/cli/werf_build.md 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/docs/_includes/reference/cli/werf_build.md 2026-10-20 17:16:12.000000000 +0200 @@ -262,6 +262,10 @@ repo Harbor username (default $WERF_REPO_HARBOR_USERNAME) --repo-quay-token="" repo quay.io token (default $WERF_REPO_QUAY_TOKEN) + -Z, --require-built-images=false + Publish previously built images and custom tags without running build instructions. + Reuse stages from primary and secondary repos; fail if a required stage is missing. + Incompatible with --check-built-images (default $WERF_REQUIRE_BUILT_IMAGES) --save-build-report=false Save build report (by default $WERF_SAVE_BUILD_REPORT or false). Its path and format configured with --build-report-path diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/docs/pages_en/usage/build/process.md new/werf-2.84.0/docs/pages_en/usage/build/process.md --- old/werf-2.83.0/docs/pages_en/usage/build/process.md 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/docs/pages_en/usage/build/process.md 2026-10-20 17:16:12.000000000 +0200 @@ -169,12 +169,12 @@ ### Checking that images are built -`werf build --check-built-images` (aliases: `--require-built-images`, `-Z`, `$WERF_CHECK_BUILT_IMAGES`), and `--require-built-images` on the commands that process images without building them, check that every image the project needs is already published. Missing stages produce `stages required`; unavailable output images, custom tags or legacy import metadata also cause the check to fail. +`werf build --check-built-images` (or `$WERF_CHECK_BUILT_IMAGES`), and `--require-built-images` on the commands that process images without building them, check that every image the project needs is already published. Missing stages produce `stages required`; unavailable output images, custom tags or legacy import metadata also cause the check to fail. The check is read-only, and stage discovery is limited to the main repository: - a stage found in a `--secondary-repo` is **not** promoted into the main repository, and secondary stage lists are not requested — so a project whose stages only exist in a secondary repository fails the check until a regular build copies them over; -- nothing is published: no stage, no manifest list for a multi-platform image, no custom tag, no managed-image record and no Git metadata. Custom tags are verified to exist instead of being created. `werf build --check-built-images` and its aliases also skip synchronization registration and use host-local locks; other commands using `--require-built-images` retain their normal synchronization initialization. The automatic host cleanup is not run either, so a check never deletes local cache data or local backend images. +- nothing is published: no stage, no manifest list for a multi-platform image, no custom tag, no managed-image record and no Git metadata. Custom tags are verified to exist instead of being created. `werf build --check-built-images` also skips synchronization registration and uses host-local locks; other commands using `--require-built-images` retain their normal synchronization initialization. The automatic host cleanup is not run either, so a check never deletes local cache data or local backend images. - existing legacy import metadata may be read from primary or secondary storage without copying it. If import resolution yields missing, broken or empty metadata, the check fails instead of generating a checksum; run a regular build to create or repair that metadata; - the configured output is still validated, read-only: with a `--final-repo`, the final image is required to exist there and is not copied into it, so the check never reports an image as available at an address that does not have it. @@ -182,6 +182,18 @@ `werf stages copy` also transfers existing legacy import metadata whose checksums match the copied stages, including registry/archive round-trips and copies selected by a build report. Old archives without that metadata still require a regular build after restoring them before read-only checks can succeed. +### Publishing previously built images + +`werf build --require-built-images` (or `-Z`, `WERF_REQUIRE_BUILT_IMAGES=true`) reuses primary and secondary stages and publishes images, custom tags and metadata without running build instructions. If a required stage is missing, the command fails. The optional `--final-repo` selects a separate destination; without it, publication uses the primary repository. Normal synchronization and automatic host cleanup remain enabled. Check and require modes cannot be enabled together. + +Legacy Stapel imports may still require a container backend: when import metadata is missing, broken or empty, require mode runs a checksum container and publishes the regenerated metadata. This does not execute stage build instructions. + +```shell +werf build --require-built-images --repo registry.example.org/stage --final-repo registry.example.org/prod +``` + +`build --require-built-images` and `-Z` no longer act as aliases for `--check-built-images`: they may write to registries and need the corresponding permissions. Replace them with `--check-built-images` in read-only validation jobs. `WERF_REQUIRE_BUILT_IMAGES`, previously ignored by `build`, now prohibits running build instructions; unset it or use `--require-built-images=false` for ordinary builds. + ## Parallelism and image assembly order <!-- reference: https://werf.io/docs/v2/internals/build_process.html#parallel-build --> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/docs/pages_ru/usage/build/process.md new/werf-2.84.0/docs/pages_ru/usage/build/process.md --- old/werf-2.83.0/docs/pages_ru/usage/build/process.md 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/docs/pages_ru/usage/build/process.md 2026-10-20 17:16:12.000000000 +0200 @@ -168,12 +168,12 @@ ### Проверка того, что образы собраны -`werf build --check-built-images` (алиасы: `--require-built-images`, `-Z`, `$WERF_CHECK_BUILT_IMAGES`), а также `--require-built-images` у команд, которые работают с образами, но не собирают их, проверяют, что все необходимые проекту образы уже опубликованы. Отсутствие стадий приводит к ошибке `stages required`; недоступность выходных образов, произвольных тегов или метаданных legacy import также завершает проверку ошибкой. +`werf build --check-built-images` (или `$WERF_CHECK_BUILT_IMAGES`), а также `--require-built-images` у команд, которые работают с образами, но не собирают их, проверяют, что все необходимые проекту образы уже опубликованы. Отсутствие стадий приводит к ошибке `stages required`; недоступность выходных образов, произвольных тегов или метаданных legacy import также завершает проверку ошибкой. Проверка выполняется только на чтение, а поиск стадий ограничен основным репозиторием: - стадия, найденная в `--secondary-repo`, **не** копируется в основной репозиторий, а списки стадий вторичных репозиториев не запрашиваются — поэтому проект, стадии которого есть только во вторичном репозитории, не пройдёт проверку, пока обычная сборка не скопирует их; -- ничего не публикуется: ни стадии, ни manifest list для мультиплатформенного образа, ни произвольные теги, ни записи managed-образов, ни Git-метаданные. Произвольные теги вместо создания проверяются на существование. `werf build --check-built-images` и его алиасы также пропускают регистрацию синхронизации и используют локальные блокировки; остальные команды с `--require-built-images` сохраняют обычную инициализацию синхронизации. Автоматическая очистка хоста тоже не запускается, поэтому проверка никогда не удаляет данные локального кеша или локальные образы бэкенда. +- ничего не публикуется: ни стадии, ни manifest list для мультиплатформенного образа, ни произвольные теги, ни записи managed-образов, ни Git-метаданные. Произвольные теги вместо создания проверяются на существование. `werf build --check-built-images` также пропускает регистрацию синхронизации и использует локальные блокировки; остальные команды с `--require-built-images` сохраняют обычную инициализацию синхронизации. Автоматическая очистка хоста тоже не запускается, поэтому проверка никогда не удаляет данные локального кеша или локальные образы бэкенда. - существующие метаданные legacy import могут читаться из основного или вторичного хранилища без копирования. Если при разрешении импорта метаданные отсутствуют, повреждены или содержат пустую контрольную сумму, проверка завершается ошибкой вместо генерации checksum; выполните обычную сборку, чтобы создать или восстановить метаданные; - настроенный результат сборки по-прежнему проверяется, только на чтение: при `--final-repo` финальный образ обязан существовать там и не копируется туда, поэтому проверка никогда не сообщает об образе как о доступном по адресу, где его нет. @@ -181,6 +181,18 @@ `werf stages copy` также переносит существующие метаданные legacy import, контрольные суммы которых совпадают с импортами скопированных стадий, включая перенос через архив и выбор стадий по отчёту сборки. После восстановления старых архивов без этих метаданных по-прежнему требуется обычная сборка, прежде чем проверка только на чтение сможет завершиться успешно. +### Публикация готовых образов + +`werf build --require-built-images` (или `-Z`, `WERF_REQUIRE_BUILT_IMAGES=true`) использует готовые стадии из основного и вторичных репозиториев и публикует образы, произвольные теги и метаданные без выполнения инструкций сборки. Если нужной стадии нет, команда завершается с ошибкой. Необязательный `--final-repo` задаёт отдельное назначение; без него публикация выполняется в основной репозиторий. Обычная синхронизация и автоматическая очистка хоста остаются включены. Режимы check и require нельзя включить одновременно. + +Для legacy Stapel import может потребоваться контейнерный бэкенд: если метаданные импорта отсутствуют, повреждены или содержат пустую контрольную сумму, require запускает контейнер расчёта checksum и публикует восстановленные метаданные. Инструкции сборки стадий при этом не выполняются. + +```shell +werf build --require-built-images --repo registry.example.org/stage --final-repo registry.example.org/prod +``` + +`build --require-built-images` и `-Z` больше не являются алиасами `--check-built-images`: они могут записывать данные в registry и требуют соответствующих прав. Для проверок без записи замените их на `--check-built-images`. Переменная `WERF_REQUIRE_BUILT_IMAGES`, которую `build` раньше игнорировал, теперь запрещает выполнение инструкций сборки; для обычной сборки уберите её или укажите `--require-built-images=false`. + ## Параллельность и порядок сборки образов <!-- прим. для перевода: на основе https://werf.io/docs/v2/internals/build_process.html#parallel-build --> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/build/build_phase.go new/werf-2.84.0/pkg/build/build_phase.go --- old/werf-2.83.0/pkg/build/build_phase.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/pkg/build/build_phase.go 2026-10-20 17:16:12.000000000 +0200 @@ -41,7 +41,8 @@ } type BuildOptions struct { - ImageBuildOptions container_backend.BuildOptions + RequireBuiltImages bool + ImageBuildOptions container_backend.BuildOptions IntrospectOptions ReportPath string @@ -723,6 +724,9 @@ } if !foundSuitableSecondaryStage { + if phase.RequireBuiltImages { + return fmt.Errorf("image %q: required stage %s with digest %s not found; build images first without --require-built-images or WERF_REQUIRE_BUILT_IMAGES", img.GetName(), stg.Name(), stg.GetDigest()) + } if phase.ShouldBeBuiltMode { phase.printShouldBeBuiltError(ctx, img, stg) return fmt.Errorf("stages required") @@ -890,7 +894,11 @@ ScanSecondaryStagesStorageList: for _, secondaryStagesStorage := range storageManager.GetSecondaryStagesStorageList() { - secondaryStages, err := storageManager.GetStageDescSetByDigestFromStagesStorageCached(ctx, stg.LogDetailedName(), stg.GetDigest(), phase.getPrevNonEmptyStageCreationTs(), secondaryStagesStorage) + lookup := storageManager.GetStageDescSetByDigestFromStagesStorageCached + if phase.RequireBuiltImages { + lookup = storageManager.GetStageDescSetByDigestFromStagesStorageWithCache + } + secondaryStages, err := lookup(ctx, stg.LogDetailedName(), stg.GetDigest(), phase.getPrevNonEmptyStageCreationTs(), secondaryStagesStorage) if err != nil { return false, err } else { @@ -973,7 +981,7 @@ storageManager := phase.Conveyor.StorageManager var stageDescSet imagePkg.StageDescSet - if phase.ShouldBeBuiltMode { + if phase.ShouldBeBuiltMode || phase.RequireBuiltImages { stageDescSet, err = storageManager.GetStageDescSetByDigestWithCache(ctx, stg.LogDetailedName(), stageDigest, phase.getPrevNonEmptyStageCreationTs()) } else { stageDescSet, err = storageManager.GetStageDescSetByDigestFromStagesStorageCached(ctx, stg.LogDetailedName(), stageDigest, phase.getPrevNonEmptyStageCreationTs(), storageManager.GetStagesStorage()) @@ -1468,7 +1476,7 @@ - manually with werf purge or werf host purge commands`) logboek.Context(ctx).Warn().LogLn() primaryStagesStorageAddress := phase.Conveyor.StorageManager.GetStagesStorage().String() - logboek.Context(ctx).Warn().LogLn(reasonNumberFunc() + fmt.Sprintf(`You are checking that images are already built (--check-built-images / --require-built-images / -Z flag, WERF_CHECK_BUILT_IMAGES / WERF_REQUIRE_BUILT_IMAGES env), so only the primary stages storage %[1]s has been searched: + logboek.Context(ctx).Warn().LogLn(reasonNumberFunc() + fmt.Sprintf(`You are checking that images are already built (e.g. werf build --check-built-images or WERF_CHECK_BUILT_IMAGES), so only the primary stages storage %[1]s has been searched: - If you expect images to be built and available in the registry, check the reasons above - Secondary stages storages (--secondary-repo and the implicit local one) are not consulted by the check and no stage is promoted into %[1]s - If the stage exists in a secondary stages storage only, publish it with an ordinary build first: werf build --repo %[1]s diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/build/check_mode_writes_test.go new/werf-2.84.0/pkg/build/check_mode_writes_test.go --- old/werf-2.83.0/pkg/build/check_mode_writes_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/pkg/build/check_mode_writes_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -121,15 +121,26 @@ ginkgo.Entry("multiplatform", true, []string{"PostMultiplatformImage", "AddStageCustomTag"}), ) - ginkgo.DescribeTable("publishes outside check mode", func(ctx ginkgo.SpecContext, publish func(phase *BuildPhase) error, expectedWrites []string) { + ginkgo.DescribeTable("publishes outside check mode", func(ctx ginkgo.SpecContext, require bool, publish func(phase *BuildPhase) error, expectedWrites []string) { phase, stagesStorage := newPhase(false) + phase.RequireBuiltImages = require gomega.Expect(publish(phase)).To(gomega.Succeed()) gomega.Expect(stagesStorage.writes).To(gomega.ConsistOf(expectedWrites)) }, - ginkgo.Entry("a single-platform image", func(phase *BuildPhase) error { + ginkgo.Entry("a single-platform image", false, func(phase *BuildPhase) error { return phase.publishImageMetadata(context.Background(), "image0", singlePlatformImage()) }, []string{"AddManagedImage", "PutImageMetadata", "AddStageCustomTag", "RegisterStageCustomTag"}), - ginkgo.Entry("a multiplatform image", func(phase *BuildPhase) error { + ginkgo.Entry("a multiplatform image", false, func(phase *BuildPhase) error { + img := multiplatformImage() + if err := phase.publishMultiplatformImageMetadata(context.Background(), "image0", img); err != nil { + return err + } + return phase.publishMultiplatformImageCustomTags(context.Background(), "image0", img) + }, []string{"AddManagedImage", "PostMultiplatformImage", "PutImageMetadata", "AddStageCustomTag", "RegisterStageCustomTag"}), + ginkgo.Entry("a required single-platform image", true, func(phase *BuildPhase) error { + return phase.publishImageMetadata(context.Background(), "image0", singlePlatformImage()) + }, []string{"AddManagedImage", "PutImageMetadata", "AddStageCustomTag", "RegisterStageCustomTag"}), + ginkgo.Entry("a required multiplatform image", true, func(phase *BuildPhase) error { img := multiplatformImage() if err := phase.publishMultiplatformImageMetadata(context.Background(), "image0", img); err != nil { return err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/build/helpers_test.go new/werf-2.84.0/pkg/build/helpers_test.go --- old/werf-2.83.0/pkg/build/helpers_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/pkg/build/helpers_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -423,3 +423,53 @@ func (m *publicationStorageManager) GetStageDescSetByDigestFromStagesStorageCached(context.Context, string, string, int64, storage.StagesStorage) (imagePkg.StageDescSet, error) { return imagePkg.NewStageDescSet(), nil } + +type requireLookupStorageManager struct{ *publicationStorageManager } + +var _ manager.StorageManagerInterface = (*requireLookupStorageManager)(nil) + +func (m *requireLookupStorageManager) GetStageDescSetByDigestWithCache(ctx context.Context, name, digest string, parentTs int64) (imagePkg.StageDescSet, error) { + return m.GetStageDescSetByDigest(ctx, name, digest, parentTs) +} + +func (m *publicationStorageManager) GetStageDescSetByDigestFromStagesStorageWithCache(ctx context.Context, name, digest string, parentTs int64, stagesStorage storage.StagesStorage) (imagePkg.StageDescSet, error) { + return m.GetStageDescSetByDigestFromStagesStorageCached(ctx, name, digest, parentTs, stagesStorage) +} + +type secondaryLookupStorageManager struct { + *publicationStorageManager + lookup *manager.StorageManager + secondary storage.StagesStorage +} + +var _ manager.StorageManagerInterface = (*secondaryLookupStorageManager)(nil) + +func (m *secondaryLookupStorageManager) GetSecondaryStagesStorageList() []storage.StagesStorage { + return []storage.StagesStorage{m.secondary} +} + +func (m *secondaryLookupStorageManager) GetStageDescSetByDigestFromStagesStorageCached(ctx context.Context, name, digest string, parentTs int64, stagesStorage storage.StagesStorage) (imagePkg.StageDescSet, error) { + return m.lookup.GetStageDescSetByDigestFromStagesStorageCached(ctx, name, digest, parentTs, stagesStorage) +} + +func (m *secondaryLookupStorageManager) GetStageDescSetByDigestFromStagesStorageWithCache(ctx context.Context, name, digest string, parentTs int64, stagesStorage storage.StagesStorage) (imagePkg.StageDescSet, error) { + return m.lookup.GetStageDescSetByDigestFromStagesStorageWithCache(ctx, name, digest, parentTs, stagesStorage) +} + +func (m *secondaryLookupStorageManager) CopySuitableStageDescByDigest(_ context.Context, desc *imagePkg.StageDesc, _, _ storage.StagesStorage, _ container_backend.ContainerBackend, _ string) (*imagePkg.StageDesc, error) { + copied := *desc + info := *desc.Info + info.Name = "repo:shared-digest-100" + copied.Info = &info + m.primary.desc = &copied + m.primary.writes++ + return &copied, nil +} + +type publicationContainerBackend struct { + container_backend.ContainerBackend +} + +var _ container_backend.ContainerBackend = (*publicationContainerBackend)(nil) + +func (b *publicationContainerBackend) HasStapelBuildSupport() bool { return true } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/build/require_built_images_test.go new/werf-2.84.0/pkg/build/require_built_images_test.go --- old/werf-2.83.0/pkg/build/require_built_images_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.84.0/pkg/build/require_built_images_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -0,0 +1,116 @@ +package build + +import ( + "net/http/httptest" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + "github.com/werf/werf/v2/pkg/build/stage" + "github.com/werf/werf/v2/pkg/config" + "github.com/werf/werf/v2/pkg/docker_registry" + imagePkg "github.com/werf/werf/v2/pkg/image" + "github.com/werf/werf/v2/pkg/storage" + "github.com/werf/werf/v2/pkg/storage/manager" + "github.com/werf/werf/v2/pkg/werf" +) + +var _ = ginkgo.Describe("Require built images", func() { + ginkgo.It("reuses a published stage despite a stale empty stages snapshot", func(ctx ginkgo.SpecContext) { + server, _ := newPublicationLockServer() + primary := &publicationStorage{desc: &imagePkg.StageDesc{ + StageID: imagePkg.NewStageID("shared-digest", 100), + Info: &imagePkg.Info{Name: "repo:shared-digest-100", Labels: map[string]string{imagePkg.WerfStageContentDigestLabel: "published-content"}}, + }} + storageManager := &publicationStorageManager{primary: primary} + phase, img, stg := newPublicationPhase(ctx, storageManager, server.URL) + phase.Conveyor.StorageManager = &requireLookupStorageManager{storageManager} + phase.RequireBuiltImages = true + reported := &reportedStage{buildableStage: &buildableStage{publicationStage: stg.(*publicationStage)}} + + gomega.Expect(phase.onImageStage(ctx, img, reported)).To(gomega.Succeed()) + gomega.Expect(reported.GetStageImage().Image.GetStageDesc()).To(gomega.Equal(primary.desc)) + gomega.Expect(primary.writes).To(gomega.BeZero()) + gomega.Expect(reported.GetStageImage().Builder.(*stageBuilderStub).builds).To(gomega.BeZero()) + }) + + ginkgo.It("reuses a secondary stage published after the cached empty snapshot", func(ctx ginkgo.SpecContext) { + gomega.Expect(werf.Init(ginkgo.GinkgoT().TempDir(), "")).To(gomega.Succeed()) + oldCache := imagePkg.CommonManifestCache + imagePkg.CommonManifestCache = imagePkg.NewManifestCache(ginkgo.GinkgoT().TempDir()) + ginkgo.DeferCleanup(func() { imagePkg.CommonManifestCache = oldCache }) + registryServer := httptest.NewServer(registry.New()) + ginkgo.DeferCleanup(registryServer.Close) + address := strings.TrimPrefix(registryServer.URL, "http://") + "/project" + client, err := docker_registry.NewDockerRegistry(ctx, address, docker_registry.DefaultImplementationName, docker_registry.DockerRegistryOptions{InsecureRegistry: true}) + gomega.Expect(err).To(gomega.Succeed()) + secondary := storage.NewRepoStagesStorage(&storage.NewRepoStagesStorageOptions{RepoAddress: address, DockerRegistry: client}) + primary := &publicationStorage{} + publicationManager := &publicationStorageManager{primary: primary} + storageManager := &secondaryLookupStorageManager{publicationStorageManager: publicationManager, lookup: &manager.StorageManager{ProjectName: "publication-project", StagesStorage: secondary}, secondary: secondary} + server, _ := newPublicationLockServer() + phase, img, stg := newPublicationPhase(ctx, publicationManager, server.URL) + phase.Conveyor.StorageManager = storageManager + phase.RequireBuiltImages = true + reported := &reportedStage{buildableStage: &buildableStage{publicationStage: stg.(*publicationStage)}} + _, cleanup, err := phase.calculateStage(ctx, img, reported) + gomega.Expect(err).To(gomega.Succeed()) + if cleanup != nil { + cleanup() + } + stageID := *imagePkg.NewStageID(reported.GetDigest(), 1611836746968) + cached, err := storageManager.GetStageDescSetByDigestFromStagesStorageCached(ctx, "stage", stageID.Digest, 10, secondary) + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(cached.IsEmpty()).To(gomega.BeTrue()) + containerImage, err := mutate.ConfigFile(empty.Image, &v1.ConfigFile{OS: "linux", Architecture: "amd64", Config: v1.Config{Labels: map[string]string{imagePkg.WerfStageContentDigestLabel: "published-content", imagePkg.WerfLabel: "publication-project"}}}) + gomega.Expect(err).To(gomega.Succeed()) + tag, err := name.NewTag(secondary.ConstructStageImageName("publication-project", stageID.Digest, stageID.CreationTs)) + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(remote.Write(tag, containerImage, remote.WithContext(ctx))).To(gomega.Succeed()) + cached, err = storageManager.GetStageDescSetByDigestFromStagesStorageCached(ctx, "stage", stageID.Digest, 10, secondary) + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(cached.IsEmpty()).To(gomega.BeTrue()) + + gomega.Expect(phase.onImageStage(ctx, img, reported)).To(gomega.Succeed()) + gomega.Expect(primary.writes).To(gomega.Equal(1)) + gomega.Expect(primary.desc.StageID).To(gomega.Equal(&stageID)) + gomega.Expect(reported.GetContentDigest()).To(gomega.Equal("published-content")) + gomega.Expect(reported.GetStageImage().Builder.(*stageBuilderStub).builds).To(gomega.BeZero()) + }) + + ginkgo.DescribeTable("does not build a missing stage when built images are required", + func(ctx ginkgo.SpecContext, require, dockerfile bool) { + server, _ := newPublicationLockServer() + primary := &publicationStorage{} + phase, img, stg := newPublicationPhase(ctx, &publicationStorageManager{primary: primary}, server.URL) + phase.RequireBuiltImages = require + phase.Conveyor.ContainerBackend = &publicationContainerBackend{} + img.IsDockerfileImage = dockerfile + img.DockerfileImageConfig = &config.ImageFromDockerfile{} + reported := &reportedStage{buildableStage: &buildableStage{publicationStage: stg.(*publicationStage)}} + img.SetStages([]stage.Interface{reported}) + + err := phase.onImageStage(ctx, img, reported) + if require { + gomega.Expect(err).To(gomega.MatchError(gomega.ContainSubstring("build images first without --require-built-images"))) + gomega.Expect(primary.writes).To(gomega.BeZero()) + gomega.Expect(reported.GetStageImage().Builder).To(gomega.BeNil()) + } else { + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(primary.writes).To(gomega.Equal(1)) + gomega.Expect(reported.GetStageImage().Builder.(*stageBuilderStub).builds).To(gomega.Equal(1)) + } + }, + ginkgo.Entry("require fails before invoking the Dockerfile builder", true, true), + ginkgo.Entry("require fails before invoking the Stapel builder", true, false), + ginkgo.Entry("ordinary Dockerfile build invokes the builder and publishes the stage", false, true), + ginkgo.Entry("ordinary Stapel build invokes the builder and publishes the stage", false, false), + ) +}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/storage/manager/helpers_test.go new/werf-2.84.0/pkg/storage/manager/helpers_test.go --- old/werf-2.83.0/pkg/storage/manager/helpers_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/pkg/storage/manager/helpers_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -13,12 +13,28 @@ "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" + "github.com/werf/werf/v2/pkg/build/stage" "github.com/werf/werf/v2/pkg/container_backend" "github.com/werf/werf/v2/pkg/docker_registry" "github.com/werf/werf/v2/pkg/image" "github.com/werf/werf/v2/pkg/storage" ) +type fetchStage struct { + stage.Interface + stageImage *stage.StageImage +} + +var _ stage.Interface = (*fetchStage)(nil) + +func (s *fetchStage) GetStageImage() *stage.StageImage { + return s.stageImage +} + +func (s *fetchStage) LogDetailedName() string { + return "stage" +} + type lookupBackend struct { container_backend.ContainerBackend images image.ImagesList diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/storage/manager/storage_manager.go new/werf-2.84.0/pkg/storage/manager/storage_manager.go --- old/werf-2.83.0/pkg/storage/manager/storage_manager.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/pkg/storage/manager/storage_manager.go 2026-10-20 17:16:12.000000000 +0200 @@ -509,7 +509,7 @@ stageDesc, err := getStageDesc(ctx, m.ProjectName, *stageID, stagesStorage, nil, getStageDescOptions{WithLocalManifestCache: true}) if err != nil { - return nil, fmt.Errorf("error getting stage %s description from %s: %w", stageID.String(), m.FinalStagesStorage.String(), err) + return nil, fmt.Errorf("error getting stage %s description from %s: %w", stageID.String(), stagesStorage.String(), err) } pulled = false stageImage.SetStageDesc(stageDesc) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/pkg/storage/manager/storage_manager_fetch_test.go new/werf-2.84.0/pkg/storage/manager/storage_manager_fetch_test.go --- old/werf-2.83.0/pkg/storage/manager/storage_manager_fetch_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.84.0/pkg/storage/manager/storage_manager_fetch_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -0,0 +1,101 @@ +package manager + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + + "github.com/werf/logboek" + "github.com/werf/logboek/pkg/level" + "github.com/werf/werf/v2/pkg/build/stage" + "github.com/werf/werf/v2/pkg/container_backend" + "github.com/werf/werf/v2/pkg/docker_registry" + "github.com/werf/werf/v2/pkg/image" + "github.com/werf/werf/v2/pkg/storage" + "github.com/werf/werf/v2/pkg/werf" + "github.com/werf/werf/v2/test/mock" +) + +var _ = ginkgo.DescribeTable("FetchStage with a locally available cache image", + func(ctx ginkgo.SpecContext, withFinalRepo bool) { + var output bytes.Buffer + logger := logboek.NewLogger(&output, &output) + logger.SetAcceptedLevel(level.Warn) + logger.Streams().DisableStyle() + logger.Streams().DisableLineWrapping() + logCtx := logboek.NewContext(ctx, logger) + gomega.Expect(werf.Init(ginkgo.GinkgoT().TempDir(), "")).To(gomega.Succeed()) + previousCache := image.CommonManifestCache + image.CommonManifestCache = image.NewManifestCache(ginkgo.GinkgoT().TempDir()) + ginkgo.DeferCleanup(func() { image.CommonManifestCache = previousCache }) + + cacheServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path == "/v2/" { + writer.WriteHeader(http.StatusOK) + return + } + http.Error(writer, "cache descriptor access denied", http.StatusForbidden) + })) + ginkgo.DeferCleanup(cacheServer.Close) + cacheAddress := strings.TrimPrefix(cacheServer.URL, "http://") + "/cache" + cacheRegistry, err := docker_registry.NewDockerRegistry(logCtx, cacheAddress, docker_registry.DefaultImplementationName, docker_registry.DockerRegistryOptions{InsecureRegistry: true}) + gomega.Expect(err).To(gomega.Succeed()) + + backend := mock.NewMockContainerBackend(gomock.NewController(ginkgo.GinkgoT())) + cache := storage.NewRepoStagesStorage(&storage.NewRepoStagesStorageOptions{ + RepoAddress: cacheAddress, DockerRegistry: cacheRegistry, ContainerBackend: backend, + }) + manager := newManifestStorageManager(logCtx) + manager.StagesStorage.(*storage.RepoStagesStorage).ContainerBackend = backend + manager.CacheStagesStorageList = []storage.StagesStorage{cache} + if withFinalRepo { + manager.FinalStagesStorage = storage.NewRepoStagesStorage(&storage.NewRepoStagesStorageOptions{RepoAddress: "registry.example/final"}) + } + ginkgo.DeferCleanup(func() { + for _, lock := range manager.SharedHostImagesLocks { + gomega.Expect(werf.HostLocker().ReleaseLock(lock)).To(gomega.Succeed()) + } + }) + + stageID := image.NewStageID(missingStageDigest, 100) + primaryName := manager.StagesStorage.ConstructStageImageName(manager.ProjectName, stageID.Digest, stageID.CreationTs) + primaryRef, err := name.NewTag(primaryName, name.Insecure) + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(remote.Write(primaryRef, empty.Image, remote.WithContext(ctx))).To(gomega.Succeed()) + primaryImage := container_backend.NewLegacyStageImage(nil, primaryName, backend, "linux/amd64") + primaryImage.SetStageDesc(&image.StageDesc{StageID: stageID}) + cacheName := cache.ConstructStageImageName(manager.ProjectName, stageID.Digest, stageID.CreationTs) + gomock.InOrder( + backend.EXPECT().GetImageInfo(gomock.Any(), primaryName, container_backend.GetImageInfoOpts{TargetPlatform: "linux/amd64"}).Return(nil, nil), + backend.EXPECT().GetImageInfo(gomock.Any(), cacheName, container_backend.GetImageInfoOpts{TargetPlatform: "linux/amd64"}).Return(&image.Info{Name: cacheName}, nil), + backend.EXPECT().PullImageFromRegistry(gomock.Any(), primaryImage).Return(nil), + ) + + var info FetchStageInfo + gomega.Expect(func() { + info, err = manager.FetchStage(logCtx, backend, &fetchStage{stageImage: &stage.StageImage{Image: primaryImage}}) + }).NotTo(gomega.Panic()) + gomega.Expect(err).To(gomega.Succeed()) + gomega.Expect(info.BaseImageSource).To(gomega.Equal(BaseImageSourceTypeRepo)) + gomega.Expect(primaryImage.GetStageDesc().Info.Name).To(gomega.Equal(primaryName)) + gomega.Expect(strings.Split(output.String(), "\n")).To(gomega.ContainElement(gomega.And( + gomega.HavePrefix(fmt.Sprintf( + "Unable to fetch stage %s from cache stages storage %s: error getting stage %s description from %s:", + stageID.String(), cache.String(), stageID.String(), cache.String(), + )), + gomega.ContainSubstring("cache descriptor access denied"), + ))) + gomega.Expect(output.String()).NotTo(gomega.ContainSubstring("registry.example/final")) + }, + ginkgo.Entry("warns and falls back after a descriptor error without final-repo", false), + ginkgo.Entry("reports the cache address rather than final-repo after a descriptor error", true), +) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.83.0/test/e2e/build/check_built_images_test.go new/werf-2.84.0/test/e2e/build/check_built_images_test.go --- old/werf-2.83.0/test/e2e/build/check_built_images_test.go 2026-10-15 12:45:06.000000000 +0200 +++ new/werf-2.84.0/test/e2e/build/check_built_images_test.go 2026-10-20 17:16:12.000000000 +0200 @@ -5,17 +5,26 @@ "os/exec" "slices" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/werf/werf/v2/test/pkg/suite_init" "github.com/werf/werf/v2/test/pkg/utils" "github.com/werf/werf/v2/test/pkg/werf" ) type checkBuiltImagesTestOptions struct { setupEnvOptions - Args []string - Env map[string]string + Args []string + Env map[string]string + Require bool +} + +var checkBuiltImagesEnv = setupEnvOptions{ + ContainerBackendMode: "docker", + WithLocalRepo: true, } var _ = Describe("Check built images", Label("e2e", "build", "simple"), func() { @@ -45,7 +54,11 @@ var checkExitErr *exec.ExitError Expect(errors.As(checkErr, &checkExitErr)).To(BeTrue(), "expected werf build to exit with a non-zero status, got: %v", checkErr) checkOut := string(checkOutBytes) - Expect(checkOut).To(ContainSubstring("stages required")) + if opts.Require { + Expect(checkOut).To(ContainSubstring("build images first without --require-built-images")) + } else { + Expect(checkOut).To(ContainSubstring("stages required")) + } Expect(checkOut).NotTo(ContainSubstring("Building stage")) By("state0: building images") @@ -65,51 +78,74 @@ Expect(checkOut).NotTo(ContainSubstring("Building stage")) }, Entry("with --check-built-images", checkBuiltImagesTestOptions{ - setupEnvOptions: setupEnvOptions{ - ContainerBackendMode: "vanilla-docker", - WithLocalRepo: true, - }, - Args: []string{"--check-built-images"}, + setupEnvOptions: checkBuiltImagesEnv, + Args: []string{"--check-built-images"}, }), Entry("with --require-built-images", checkBuiltImagesTestOptions{ - setupEnvOptions: setupEnvOptions{ - ContainerBackendMode: "vanilla-docker", - WithLocalRepo: true, - }, - Args: []string{"--require-built-images"}, + setupEnvOptions: checkBuiltImagesEnv, + Args: []string{"--require-built-images"}, + Require: true, + }), + Entry("with --require-built-images for Stapel only", checkBuiltImagesTestOptions{ + setupEnvOptions: checkBuiltImagesEnv, + Args: []string{"stapel-shell", "--require-built-images"}, + Require: true, + }), + Entry("with --require-built-images for Stapel only using Buildah", checkBuiltImagesTestOptions{ + setupEnvOptions: setupEnvOptions{ContainerBackendMode: "native-rootless", WithLocalRepo: true}, + Args: []string{"stapel-shell", "--require-built-images"}, + Require: true, }), Entry("with -Z", checkBuiltImagesTestOptions{ - setupEnvOptions: setupEnvOptions{ - ContainerBackendMode: "vanilla-docker", - WithLocalRepo: true, - }, - Args: []string{"-Z"}, + setupEnvOptions: checkBuiltImagesEnv, + Args: []string{"-Z"}, + Require: true, }), Entry("with WERF_CHECK_BUILT_IMAGES", checkBuiltImagesTestOptions{ - setupEnvOptions: setupEnvOptions{ - ContainerBackendMode: "vanilla-docker", - WithLocalRepo: true, - }, - Env: map[string]string{"WERF_CHECK_BUILT_IMAGES": "1"}, + setupEnvOptions: checkBuiltImagesEnv, + Env: map[string]string{"WERF_CHECK_BUILT_IMAGES": "1"}, + }), + Entry("with WERF_REQUIRE_BUILT_IMAGES", checkBuiltImagesTestOptions{ + setupEnvOptions: checkBuiltImagesEnv, + Env: map[string]string{"WERF_REQUIRE_BUILT_IMAGES": "1"}, + Require: true, }), ) +}) - It("should build images regardless of WERF_REQUIRE_BUILT_IMAGES", func(ctx SpecContext) { - By("initializing") - setupEnv(setupEnvOptions{ - ContainerBackendMode: "vanilla-docker", - WithLocalRepo: true, - }) - SuiteData.Stubs.SetEnv("WERF_REQUIRE_BUILT_IMAGES", "1") - - By("state0: preparing test repo") - repoDirname := "repo0" - SuiteData.InitTestRepo(ctx, repoDirname, "simple/state0") - werfProject := werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath(repoDirname)) - - By("state0: building images against an empty repo") - buildOut := werfProject.Build(ctx, nil) - Expect(buildOut).To(ContainSubstring("Building stage")) - Expect(buildOut).NotTo(ContainSubstring("stages required")) - }) +var _ = Describe("Require built images publication", Label("e2e", "build", "simple"), func() { + DescribeTable("copies secondary stages and publishes the final repository without building", func(ctx SpecContext, opts setupEnvOptions) { + setupEnv(opts) + SuiteData.InitTestRepo(ctx, "repo0", "simple/state0") + project := werf.NewProject(SuiteData.WerfBinPath, SuiteData.GetTestRepoPath("repo0")) + project.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ExtraArgs: []string{"dockerfile"}}}) + + source := suite_init.TestRepo(SuiteData.ProjectName) + primary := suite_init.TestRepo(SuiteData.ProjectName + "-required") + finalRepo := suite_init.TestRepo(SuiteData.ProjectName + "-final") + args := []string{"dockerfile", "--repo", primary, "--secondary-repo", source, "--add-custom-tag", "release"} + args = append(args, "--final-repo", finalRepo) + + checkOutput := project.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ + ShouldFail: true, + ExtraArgs: slices.Concat(args, []string{"--check-built-images"}), + }}) + Expect(checkOutput).To(ContainSubstring("stages required")) + + output := project.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ + ExtraArgs: slices.Concat(args, []string{"--require-built-images"}), + }}) + Expect(output).NotTo(ContainSubstring("Building stage")) + tag, err := name.NewTag(finalRepo+":release", name.Insecure) + Expect(err).NotTo(HaveOccurred()) + manifest, err := remote.Get(tag, remote.WithContext(ctx)) + Expect(err).NotTo(HaveOccurred()) + Expect(manifest.Digest.String()).NotTo(BeEmpty()) + project.Build(ctx, &werf.BuildOptions{CommonOptions: werf.CommonOptions{ + ExtraArgs: slices.Concat(args, []string{"--check-built-images"}), + }}) + }, + Entry("Docker", setupEnvOptions{ContainerBackendMode: "docker", WithLocalRepo: true}), + Entry("Buildah", setupEnvOptions{ContainerBackendMode: "native-rootless", WithLocalRepo: true}), + ) }) ++++++ werf.obsinfo ++++++ --- /var/tmp/diff_new_pack.poTGC7/_old 2026-11-13 18:39:01.941698681 +0100 +++ /var/tmp/diff_new_pack.poTGC7/_new 2026-11-13 18:39:01.946698893 +0100 @@ -1,5 +1,5 @@ name: werf -version: 2.83.0 -mtime: 1792061106 -commit: 393e5d5e9fe4bea56b226174e3d4397bc78ce602 +version: 2.84.0 +mtime: 1792509372 +commit: 4ca847c4b39ff5174f9174b41613133f4dfdcd62
