Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/cli/cmd/attestation_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func newAttestationInitCmd() *cobra.Command {
cmd.Flags().BoolVar(&existingVersion, "existing-version", false, "return an error if the version doesn't exist in the project")
cmd.Flags().StringSliceVar(&collectors, "collectors", nil, "comma-separated list of additional collectors to enable (e.g. aiconfig)")
cmd.Flags().BoolVar(&markAsLatest, "mark-latest", true, "explicitly mark the project version as latest (default: automatic for new versions; use =false to skip promotion)")
cmd.Flags().BoolVar(&prMode, "pr", false, "mark this attestation as a pull/merge request build (sets the chainloop.dev/is-pull-request annotation; auto-detected from CI env if not set)")
cmd.Flags().BoolVar(&prMode, "pr", false, "mark this attestation as a pull/merge request build (skips latest promotion and sets the chainloop.dev/is-pull-request annotation; auto-detected from CI env if not set)")

return cmd
}
2 changes: 1 addition & 1 deletion app/cli/documentation/cli-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ Options
-h, --help help for init
--latest-version use the latest existing project version instead of specifying one
--mark-latest explicitly mark the project version as latest (default: automatic for new versions; use =false to skip promotion) (default true)
--pr mark this attestation as a pull/merge request build (sets the chainloop.dev/is-pull-request annotation; auto-detected from CI env if not set)
--pr mark this attestation as a pull/merge request build (skips latest promotion and sets the chainloop.dev/is-pull-request annotation; auto-detected from CI env if not set)
--project string name of the project of this workflow
--release promote the provided version as a release
--remote-state Store the attestation state remotely
Expand Down
16 changes: 13 additions & 3 deletions app/cli/pkg/action/attestation_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,16 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
}

// Resolve PR mode: explicit --pr flag wins, otherwise auto-detect from the
// CI runner environment. PR mode only tags the attestation with the
// chainloop.dev/is-pull-request annotation; it does not alter latest
// promotion (use --mark-latest=false explicitly to skip promotion).
// CI runner environment. When in PR mode, default mark-latest=false unless
// the user explicitly passed --mark-latest=true.
isPR, err := action.resolvePRMode(ctx, discoveredRunner, opts.PRMode)
if err != nil {
action.Logger.Warn().Err(err).Msg("failed to detect PR context")
}
if isPR && !explicitMarkAsLatestTrue(opts.MarkAsLatest) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The PR-mode suppression branch is not exercised through AttestationInit.Run; the current helper test could pass while the request still sends the wrong pointer after a future refactor. A focused action/fake-client test covering omitted, explicit false, and explicit true inputs would lock down the three-state behavior.

(Based on your team's feedback about tests for new paths.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/pkg/action/attestation_init.go, line 203:

<comment>The PR-mode suppression branch is not exercised through `AttestationInit.Run`; the current helper test could pass while the request still sends the wrong pointer after a future refactor. A focused action/fake-client test covering omitted, explicit false, and explicit true inputs would lock down the three-state behavior.

(Based on your team's feedback about tests for new paths.) </comment>

<file context>
@@ -194,13 +194,16 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun
 	if err != nil {
 		action.Logger.Warn().Err(err).Msg("failed to detect PR context")
 	}
+	if isPR && !explicitMarkAsLatestTrue(opts.MarkAsLatest) {
+		falseVal := false
+		opts.MarkAsLatest = &falseVal
</file context>

falseVal := false
opts.MarkAsLatest = &falseVal
}

// Parse the raw contract to get V2 schema if available
var schemaV2 *v1.CraftingSchemaV2
Expand Down Expand Up @@ -566,3 +569,10 @@ func (action *AttestationInit) resolvePRMode(ctx context.Context, runner crafter
}
return detected, nil
}

// explicitMarkAsLatestTrue returns true only when the user explicitly passed
// --mark-latest=true. A nil pointer (flag not set) or an explicit false do not
// count, so PR mode can safely override them to false.
func explicitMarkAsLatestTrue(v *bool) bool {
return v != nil && *v
}
30 changes: 30 additions & 0 deletions app/cli/pkg/action/attestation_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,36 @@ func TestParseContractV2(t *testing.T) {
}
}

func TestExplicitMarkAsLatestTrue(t *testing.T) {
cases := []struct {
name string
v *bool
want bool
}{
{
name: "nil pointer (flag not set) is not explicit-true",
v: nil,
want: false,
},
{
name: "explicit false is not explicit-true",
v: boolPtr(false),
want: false,
},
{
name: "explicit true is explicit-true",
v: boolPtr(true),
want: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, explicitMarkAsLatestTrue(tc.v))
})
}
}

func TestResolvePRMode(t *testing.T) {
// resolvePRMode uses the override when provided (explicit --pr flag),
// otherwise falls back to DetectPRContext from the runner environment.
Expand Down
8 changes: 5 additions & 3 deletions app/controlplane/api/controlplane/v1/workflow_run.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions app/controlplane/api/controlplane/v1/workflow_run.proto
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ message AttestationServiceInitRequest {
// Mutually exclusive with project_version.
bool use_latest_version = 8;
// Optional flag to control whether the version should be marked as the latest.
// Omitted: default behavior (new versions become latest).
// true: force promote to latest (only pre-release versions).
// false: skip latest promotion.
// Omitted (nil): new versions become latest, and an existing newest
// pre-release version is promoted to latest when it is not already latest.
// true: force promotion of a pre-release version to latest (released
// versions are rejected).
// false: skip latest promotion (also the sentinel sent by PR mode).
// Mutually exclusive with use_latest_version.
optional bool mark_as_latest = 9;
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

160 changes: 142 additions & 18 deletions app/controlplane/pkg/biz/workflowrun_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -826,16 +826,7 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() {
})

s.T().Run("multiple new versions with mark-as-latest=false — none are latest", func(_ *testing.T) {
// Use a fresh workflow/project to start with a clean slate
wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{
Name: "ml-none-latest-wf", OrgID: s.org.ID, Project: "ml-none-latest-project",
})
s.Require().NoError(err)

// Delete the auto-created default version so we start with zero latest
_, err = s.Data.DB.ProjectVersion.Delete().
Where(entProjectVersion.ProjectID(wf.ProjectID)).Exec(ctx)
s.Require().NoError(err)
wf := s.createIsolatedWorkflow(ctx, "ml-none-latest-wf", "ml-none-latest-project")

markFalse := false
run1, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
Expand Down Expand Up @@ -863,14 +854,7 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() {
})

s.T().Run("mark-as-latest=false then mark-as-latest=true on second version", func(_ *testing.T) {
wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{
Name: "ml-false-then-true-wf", OrgID: s.org.ID, Project: "ml-false-then-true-project",
})
s.Require().NoError(err)

_, err = s.Data.DB.ProjectVersion.Delete().
Where(entProjectVersion.ProjectID(wf.ProjectID)).Exec(ctx)
s.Require().NoError(err)
wf := s.createIsolatedWorkflow(ctx, "ml-false-then-true-wf", "ml-false-then-true-project")

// First version: not latest
markFalse := false
Expand Down Expand Up @@ -978,6 +962,132 @@ func (s *workflowRunIntegrationTestSuite) TestCreate() {
s.Require().NoError(err)
s.False(run.ProjectVersion.Latest)
})

s.T().Run("omitted promotion repairs newest existing pre-release version", func(_ *testing.T) {
wf := s.createIsolatedWorkflow(ctx, "ml-repair-wf", "ml-repair-project")

// 1) v1 created normally -> latest=true.
runV1, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "repair-v1",
})
s.Require().NoError(err)
s.True(runV1.ProjectVersion.Latest)

// 2) v2 created with mark-as-latest=false (PR/explicit-false sentinel) -> v2 not latest, v1 stays latest.
markFalse := false
runV2PR, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "repair-v2", MarkAsLatest: &markFalse,
})
s.Require().NoError(err)
s.False(runV2PR.ProjectVersion.Latest)
v1AfterPR, err := s.ProjectVersion.FindByProjectAndVersion(ctx, wf.ProjectID.String(), "repair-v1")
s.Require().NoError(err)
s.True(v1AfterPR.Latest)

// 3) Re-attest v2 with mark-as-latest=nil (omitted) -> v2 is the newest active pre-release and not latest, so it is repaired to latest, and v1 becomes non-latest.
runV2Repair, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "repair-v2",
})
s.Require().NoError(err)
s.True(runV2Repair.ProjectVersion.Latest)
v1AfterRepair, err := s.ProjectVersion.FindByProjectAndVersion(ctx, wf.ProjectID.String(), "repair-v1")
s.Require().NoError(err)
s.False(v1AfterRepair.Latest)
})

s.T().Run("omitted promotion does not promote an older existing version over a newer one", func(_ *testing.T) {
wf := s.createIsolatedWorkflow(ctx, "ml-repair-older-wf", "ml-repair-older-project")

// v1 then v2, both created with nil (v2 becomes latest).
_, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "older-v1",
})
s.Require().NoError(err)
_, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "older-v2",
})
s.Require().NoError(err)

// Re-attest the older v1 with nil -> must NOT promote it over newer v2.
runOlder, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "older-v1",
})
s.Require().NoError(err)
s.False(runOlder.ProjectVersion.Latest)
v2After, err := s.ProjectVersion.FindByProjectAndVersion(ctx, wf.ProjectID.String(), "older-v2")
s.Require().NoError(err)
s.True(v2After.Latest)
})

s.T().Run("explicit false suppression wins over automatic repair on newer version", func(_ *testing.T) {
wf := s.createIsolatedWorkflow(ctx, "ml-repair-suppress-wf", "ml-repair-suppress-project")

// v1 not latest, then v2 explicit-true -> v2 latest.
markFalse := false
_, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "suppress-v1", MarkAsLatest: &markFalse,
})
s.Require().NoError(err)
markTrue := true
_, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "suppress-v2", MarkAsLatest: &markTrue,
})
s.Require().NoError(err)

// Explicitly restore v1 as latest.
_, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "suppress-v1", MarkAsLatest: &markTrue,
})
s.Require().NoError(err)
v1Restored, err := s.ProjectVersion.FindByProjectAndVersion(ctx, wf.ProjectID.String(), "suppress-v1")
s.Require().NoError(err)
s.True(v1Restored.Latest)

// Re-attest the newer v2 with mark-as-latest=false (PR/explicit-false sentinel) -> v1 stays latest, v2 not promoted.
runV2Suppressed, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "suppress-v2", MarkAsLatest: &markFalse,
})
s.Require().NoError(err)
s.False(runV2Suppressed.ProjectVersion.Latest)
v1Final, err := s.ProjectVersion.FindByProjectAndVersion(ctx, wf.ProjectID.String(), "suppress-v1")
s.Require().NoError(err)
s.True(v1Final.Latest)
})

s.T().Run("omitted promotion on newest released version does not auto-promote", func(_ *testing.T) {
wf := s.createIsolatedWorkflow(ctx, "ml-repair-released-wf", "ml-repair-released-project")

// Create the newest version as non-latest (mark=false), then release it.
// This isolates the released-version guard from the already-latest short-circuit.
markFalse := false
run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "released-newest", MarkAsLatest: &markFalse,
})
s.Require().NoError(err)
s.False(run.ProjectVersion.Latest)
_, err = s.ProjectVersion.UpdateReleaseStatus(ctx, run.ProjectVersion.ID.String(), true)
s.Require().NoError(err)

// Re-attest with nil (omitted) without the org released-version block -> succeeds, no auto-promotion.
run2, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{
WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID,
RunnerType: "runnerType", RunnerRunURL: "runURL", ProjectVersion: "released-newest",
})
s.Require().NoError(err)
// Released versions are never auto-promoted; latest flag is unchanged.
s.False(run2.ProjectVersion.Latest)
})
}

func (s *workflowRunIntegrationTestSuite) TestContractInformation() {
Expand Down Expand Up @@ -1023,6 +1133,20 @@ func TestWorkflowRunUseCase(t *testing.T) {
suite.Run(t, new(workflowRunIntegrationTestSuite))
}

// createIsolatedWorkflow creates a fresh workflow in its own project and
// deletes the auto-created default version, so a subtest starts with a clean
// latest-version slate. The caller drives the full lifecycle under test.
func (s *workflowRunIntegrationTestSuite) createIsolatedWorkflow(ctx context.Context, name, project string) *biz.Workflow {
wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{
Name: name, OrgID: s.org.ID, Project: project,
})
s.Require().NoError(err)
_, err = s.Data.DB.ProjectVersion.Delete().
Where(entProjectVersion.ProjectID(wf.ProjectID)).Exec(ctx)
s.Require().NoError(err)
return wf
}

// Utility struct to hold the test suite
type workflowRunIntegrationTestSuite struct {
testhelpers.UseCasesEachTestSuite
Expand Down
Loading
Loading