diff --git a/doc/rfc/stovepipe/steps/record.md b/doc/rfc/stovepipe/steps/record.md index 435cebeb1..5d0140027 100644 --- a/doc/rfc/stovepipe/steps/record.md +++ b/doc/rfc/stovepipe/steps/record.md @@ -62,15 +62,20 @@ For a delivery carrying a `Record` payload: ref, and no retry can promote it. - other error -> return raw. -10. Publish a validation.repository.recorded HookEvent naming the request whose fact +10. Resolve and record optional project results. + - failure -> return raw; the repository fact, bookmark, and promotion are not undone. + +11. Publish a validation.repository.recorded HookEvent naming the request whose fact was persisted. - publish failure -> return raw; non-retryable, so it dead-letters. -11. ack. +12. ack. ``` Every decision after step 6 uses the persisted fact, not the outcome read from this delivery's Request. The first immutable fact controls the bookmark, the ref, and the hook event. +Project-status processing does not delay baseline advancement or promotion. It still completes before the recorded notification. Bookmark advancement and ref promotion remain separate operations; this ordering does not make them atomic. + ## Validation facts A fact answers "how broken was this scope at this Queue URI?" Its identity is `(queue, uri, project)`, where `project` is empty for whole-repository greenness and a stable project id in Phase 2. diff --git a/stovepipe/controller/record/record.go b/stovepipe/controller/record/record.go index f3cb9956e..2b6ef37df 100644 --- a/stovepipe/controller/record/record.go +++ b/stovepipe/controller/record/record.go @@ -154,10 +154,10 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er if err := c.persistValidationFactRecordedLog(ctx, store, request, fact); err != nil { return err } - if err := c.recordProjectFacts(ctx, store, request); err != nil { + if err := c.applyFactToDerivedCaches(ctx, store, request, fact, created); err != nil { return err } - if err := c.applyFactToDerivedCaches(ctx, store, request, fact, created); err != nil { + if err := c.recordProjectFacts(ctx, store, request); err != nil { return err } return c.publishHookEvent(ctx, request, hookevent.NewValidationRepositoryRecorded(request)) diff --git a/stovepipe/controller/record/record_test.go b/stovepipe/controller/record/record_test.go index 1589eec6d..0743ef77a 100644 --- a/stovepipe/controller/record/record_test.go +++ b/stovepipe/controller/record/record_test.go @@ -334,6 +334,32 @@ func TestProcess_AdvancesBookmarkOnSuccess(t *testing.T) { } } +func TestProcess_PromotesGreenBeforeProjectResolution(t *testing.T) { + ctrl := gomock.NewController(t) + c, m := newController(t, ctrl) + request := requestWithState(entity.RequestStateSucceeded) + m.reqStore.EXPECT().Get(gomock.Any(), testID).Return(request, nil) + m.factStore.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil) + m.queueStore.EXPECT().Get(gomock.Any(), testQueue).Return(queueRow("", "", 1), nil) + m.queueStore.EXPECT().Update(gomock.Any(), gomock.Any(), int32(1), int32(2)).Return(nil) + m.sourceControl.EXPECT().ChangeInfo(gomock.Any(), testURI). + Return(sourcecontrol.ChangeInfo{CreatedAt: testChangeTime.UnixMilli()}, nil) + promote := m.sourceControl.EXPECT().Promote(gomock.Any(), testURI).Return(nil) + + resolverFactory := projectstatusmock.NewMockResolverFactory(ctrl) + resolver := projectstatusmock.NewMockResolver(ctrl) + c.projectStatusResolverFactory = resolverFactory + resolverFactory.EXPECT().For(projectstatus.ResolverConfig{QueueName: testQueue}). + After(promote). + Return(resolver, nil) + sentinel := errors.New("resolution unavailable") + resolver.EXPECT().Resolve(gomock.Any(), request, request.TerminalBuildID).Return(nil, sentinel) + + err := c.Process(queueContext(), delivery(t, ctrl, recordPayload(t, testID))) + require.ErrorIs(t, err, sentinel) + assert.Empty(t, m.hooks.events) +} + func TestProcess_RejectsLegacyBookmark(t *testing.T) { ctrl := gomock.NewController(t) c, m := newController(t, ctrl)