Skip to content

Commit d2cc3f5

Browse files
committed
Fix promotion timeout when prow diff contains no digests
When --use-prow-manifest-diff is enabled and the triggering commit only modifies promoter-manifest.yaml (without touching images.yaml), diffProwFiles returns an empty digest list. The filtering in ParseThinManifestFromFile was skipped for empty lists, causing the promoter to load all images across all staging projects. The subsequent sequential provenance verification for 900+ images caused the job to hit the 4h timeout. Return early with an empty manifest set when prow diff mode is active but no digests are found. Also stop the pipeline cleanly when ParseManifests returns no manifests. Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
1 parent 0920cc2 commit d2cc3f5

File tree

3 files changed

+48
-1
lines changed

3 files changed

+48
-1
lines changed

promoter/image/promoter.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ func (p *Promoter) PromoteImages(ctx context.Context, opts *options.Options) err
167167
return fmt.Errorf("parsing manifests: %w", err)
168168
}
169169

170+
if len(mfests) == 0 {
171+
logrus.Info("No manifests to process, nothing to promote")
172+
173+
return pipeline.ErrStopPipeline
174+
}
175+
170176
p.impl.PrintVersion()
171177

172178
promotionEdges, err = p.impl.GetPromotionEdges(ctx, opts, mfests)

promoter/image/promoter_test.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@ import (
3030
"sigs.k8s.io/promo-tools/v4/promoter/image/promotion"
3131
"sigs.k8s.io/promo-tools/v4/promoter/image/provenance"
3232
"sigs.k8s.io/promo-tools/v4/promoter/image/registry"
33+
"sigs.k8s.io/promo-tools/v4/promoter/image/schema"
3334
"sigs.k8s.io/promo-tools/v4/types/image"
3435
)
3536

37+
// nonEmptyManifests returns a minimal manifest slice so that the pipeline
38+
// does not stop early due to an empty manifest list.
39+
func nonEmptyManifests() []schema.Manifest {
40+
return []schema.Manifest{{}}
41+
}
42+
3643
func TestPromoteImages(t *testing.T) {
3744
sut := imagepromoter.Promoter{}
3845
sut.SetProvenanceGenerator(&provenance.PromotionGenerator{})
@@ -51,7 +58,9 @@ func TestPromoteImages(t *testing.T) {
5158
// No errors
5259
shouldErr: false,
5360
msg: "No errors",
54-
prepare: func(_ *imagefakes.FakePromoterImplementation) {},
61+
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
62+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
63+
},
5564
},
5665
{
5766
// ValidateOptions fails
@@ -78,27 +87,31 @@ func TestPromoteImages(t *testing.T) {
7887
// GetPromotionEdges fails
7988
shouldErr: true,
8089
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
90+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
8191
fpi.GetPromotionEdgesReturns(nil, testErr)
8292
},
8393
},
8494
{
8595
// ValidateStagingSignatures fails
8696
shouldErr: true,
8797
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
98+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
8899
fpi.ValidateStagingSignaturesReturns(nil, testErr)
89100
},
90101
},
91102
{
92103
// PromoteImages fails
93104
shouldErr: true,
94105
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
106+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
95107
fpi.PromoteImagesReturns(testErr)
96108
},
97109
},
98110
{
99111
// SignImages fails
100112
shouldErr: true,
101113
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
114+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
102115
fpi.SignImagesReturns(testErr)
103116
},
104117
},
@@ -107,6 +120,7 @@ func TestPromoteImages(t *testing.T) {
107120
shouldErr: true,
108121
msg: "WriteProvenanceAttestations fails",
109122
prepare: func(fpi *imagefakes.FakePromoterImplementation) {
123+
fpi.ParseManifestsReturns(nonEmptyManifests(), nil)
110124
fpi.WriteProvenanceAttestationsReturns(testErr)
111125
},
112126
},
@@ -126,6 +140,7 @@ func TestPromoteImages(t *testing.T) {
126140
func TestPromoteImagesParseOnly(t *testing.T) {
127141
sut := imagepromoter.Promoter{}
128142
mock := imagefakes.FakePromoterImplementation{}
143+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
129144
sut.SetImplementation(&mock)
130145

131146
// ParseOnly should stop after plan phase with no error
@@ -141,6 +156,7 @@ func TestPromoteImagesParseOnly(t *testing.T) {
141156
func TestPromoteImagesNonConfirm(t *testing.T) {
142157
sut := imagepromoter.Promoter{}
143158
mock := imagefakes.FakePromoterImplementation{}
159+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
144160
sut.SetImplementation(&mock)
145161
sut.SetProvenanceVerifier(&fakeVerifier{
146162
result: &provenance.Result{Verified: true},
@@ -156,9 +172,25 @@ func TestPromoteImagesNonConfirm(t *testing.T) {
156172
require.Equal(t, 0, mock.PromoteImagesCallCount())
157173
}
158174

175+
func TestPromoteImagesEmptyManifests(t *testing.T) {
176+
sut := imagepromoter.Promoter{}
177+
mock := imagefakes.FakePromoterImplementation{}
178+
// Return empty manifests (e.g., prow diff found no digests)
179+
mock.ParseManifestsReturns([]schema.Manifest{}, nil)
180+
sut.SetImplementation(&mock)
181+
182+
opts := &options.Options{Confirm: true}
183+
require.NoError(t, sut.PromoteImages(context.Background(), opts))
184+
185+
// No downstream phases should have been called
186+
require.Equal(t, 0, mock.GetPromotionEdgesCallCount())
187+
require.Equal(t, 0, mock.PromoteImagesCallCount())
188+
}
189+
159190
func TestPromoteImagesProvenanceAlwaysRuns(t *testing.T) {
160191
sut := imagepromoter.Promoter{}
161192
mock := imagefakes.FakePromoterImplementation{}
193+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
162194
mock.GetPromotionEdgesReturns(map[promotion.Edge]any{
163195
testEdge(): nil,
164196
}, nil)
@@ -199,6 +231,7 @@ func testEdge() promotion.Edge {
199231
func TestPromoteImagesProvenanceFails(t *testing.T) {
200232
sut := imagepromoter.Promoter{}
201233
mock := imagefakes.FakePromoterImplementation{}
234+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
202235
// Return a non-empty edge set so provenance has something to check
203236
mock.GetPromotionEdgesReturns(map[promotion.Edge]any{
204237
testEdge(): nil,
@@ -223,6 +256,7 @@ func TestPromoteImagesProvenanceFails(t *testing.T) {
223256
func TestPromoteImagesProvenanceVerifierError(t *testing.T) {
224257
sut := imagepromoter.Promoter{}
225258
mock := imagefakes.FakePromoterImplementation{}
259+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
226260
mock.GetPromotionEdgesReturns(map[promotion.Edge]any{
227261
testEdge(): nil,
228262
}, nil)
@@ -325,6 +359,7 @@ func TestNewPromoter(t *testing.T) {
325359
// Verify that a promoter created via New() has the verifier and
326360
// generator configured by running a full pipeline with a mock impl.
327361
mock := imagefakes.FakePromoterImplementation{}
362+
mock.ParseManifestsReturns(nonEmptyManifests(), nil)
328363
p.SetImplementation(&mock)
329364

330365
require.NoError(t, p.PromoteImages(context.Background(), &options.Options{Confirm: true}))

promoter/image/schema/manifest.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ func ParseThinManifestsFromDir(
264264
if err != nil {
265265
return nil, fmt.Errorf("get prow diff files: %w", err)
266266
}
267+
268+
if len(digestsToCheck) == 0 {
269+
logrus.Info("No digests found in prow diff, nothing to promote")
270+
271+
return []Manifest{}, nil
272+
}
267273
}
268274

269275
// Check that the thin manifests dir follows a regular, predefined format.

0 commit comments

Comments
 (0)