Skip to content
Open
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
15 changes: 12 additions & 3 deletions checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type Checkout struct {
// field into the same empty output.
Skip *bool `yaml:"skip,omitempty"`

CommitVerification string `json:"commit_verification,omitempty" yaml:"commit_verification,omitempty"`

// RemainingFields stores any other top-level mapping items so they at least
// survive an unmarshal-marshal round-trip.
RemainingFields map[string]any `yaml:",inline"`
Expand All @@ -33,10 +35,13 @@ func (c *Checkout) MarshalJSON() ([]byte, error) {
return inlineFriendlyMarshalJSON(c)
}

// IsEmpty reports whether the checkout is empty (is nil, or has no Skip and
// no other data within it). Used by signing to canonicalise empty/nil values.
// IsEmpty reports whether the checkout is empty (is nil, or has no Skip,
// CommitVerification, or other data within it). Used by signing to canonicalise
// empty/nil values.
func (c *Checkout) IsEmpty() bool {
return c == nil || (c.Skip == nil && len(c.RemainingFields) == 0)
return c == nil || (c.Skip == nil &&
c.CommitVerification == "" &&
len(c.RemainingFields) == 0)
}

// UnmarshalOrdered unmarshals a Checkout from an ordered map. Bool inputs are
Expand Down Expand Up @@ -76,6 +81,10 @@ func (c *Checkout) mergeFrom(parent *Checkout) {
c.Skip = &v
}

if c.CommitVerification == "" && parent.CommitVerification != "" {
c.CommitVerification = parent.CommitVerification
}

if len(parent.RemainingFields) == 0 {
return
}
Expand Down
115 changes: 115 additions & 0 deletions checkout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ func TestCheckoutMarshalYAML(t *testing.T) {
c: Checkout{Skip: ptr(false)},
want: "skip: false\n",
},
{
name: "just commit verification",
c: Checkout{CommitVerification: "strict"},
want: "commit_verification: strict\n",
},
{
name: "commit verification and skip",
c: Checkout{Skip: ptr(true), CommitVerification: "strict"},
want: "skip: true\ncommit_verification: strict\n",
},
{
name: "with remaining fields",
c: Checkout{
Expand Down Expand Up @@ -83,6 +93,16 @@ func TestCheckoutMarshalJSON(t *testing.T) {
c: Checkout{Skip: ptr(false)},
want: `{"skip":false}`,
},
{
name: "commit verification set",
c: Checkout{CommitVerification: "strict"},
want: `{"commit_verification":"strict"}`,
},
{
name: "commit verification and skip set",
c: Checkout{Skip: ptr(true), CommitVerification: "strict"},
want: `{"commit_verification":"strict","skip":true}`,
},
{
name: "with remaining fields",
c: Checkout{
Expand Down Expand Up @@ -131,6 +151,17 @@ func TestCheckoutUnmarshalYAML(t *testing.T) {
in: `skip: false`,
want: Checkout{Skip: ptr(false)},
},
{
name: "commit verification set",
in: `commit_verification: strict`,
want: Checkout{CommitVerification: "strict"},
},
{
name: "commit verification and skip set",
in: `commit_verification: strict
skip: true`,
want: Checkout{CommitVerification: "strict", Skip: ptr(true)},
},
{
name: "with extra fields",
in: `skip: true
Expand Down Expand Up @@ -284,6 +315,18 @@ func TestCheckoutRoundTripYAML(t *testing.T) {
name: "empty mapping",
in: "{}\n",
},
{
name: "verify commit survives",
in: "commit_verification: strict\n",
},
{
name: "skip and verify commit survives",
in: "skip: true\ncommit_verification: strict\n",
},
{
name: "arbitrary verification value survives",
in: "commit_verification: bananas\n",
},
{
name: "unknown fields preserved",
in: `depth: 1
Expand Down Expand Up @@ -337,6 +380,9 @@ func TestCheckoutRoundTripJSON(t *testing.T) {
{name: "skip false", in: `{"skip":false}`},
{name: "skip true", in: `{"skip":true}`},
{name: "empty", in: `{}`},
{name: "commit verification", in: `{"commit_verification":"strict"}`},
{name: "commit verification with skip", in: `{"commit_verification":"strict","skip":true}`},
{name: "commit verification arbitrary value", in: `{"commit_verification":"bananas"}`},
{name: "with remaining", in: `{"depth":1,"skip":true}`},
}

Expand Down Expand Up @@ -390,6 +436,51 @@ func TestCheckoutInterpolationNoOp(t *testing.T) {
}
}

func TestCheckoutIsEmpty(t *testing.T) {
t.Parallel()

cases := []struct {
name string
c *Checkout
want bool
}{
{
name: "nil",
c: nil,
want: true,
},
{
name: "zero value",
c: &Checkout{},
want: true,
},
{
name: "only skip set",
c: &Checkout{Skip: ptr(true)},
want: false,
},
{
name: "only commit verification set",
c: &Checkout{CommitVerification: "strict"},
want: false,
},
{
name: "only remaining fields set",
c: &Checkout{RemainingFields: map[string]any{"depth": 1}},
want: false,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := tc.c.IsEmpty(); got != tc.want {
t.Errorf("Checkout.IsEmpty() = %v, want %v", got, tc.want)
}
})
}
}

func TestCheckoutMergeFrom(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -441,6 +532,30 @@ func TestCheckoutMergeFrom(t *testing.T) {
parent: &Checkout{Skip: ptr(false)},
want: &Checkout{Skip: ptr(false)},
},
{
name: "parent only verify",
child: &Checkout{},
parent: &Checkout{CommitVerification: "strict"},
want: &Checkout{CommitVerification: "strict"},
},
{
name: "child only verify",
child: &Checkout{CommitVerification: "strict"},
parent: &Checkout{},
want: &Checkout{CommitVerification: "strict"},
},
{
name: "child verify parent empty",
child: &Checkout{CommitVerification: "strict"},
parent: &Checkout{CommitVerification: ""},
want: &Checkout{CommitVerification: "strict"},
},
{
name: "child verify beats parent verify",
child: &Checkout{CommitVerification: "warn"},
parent: &Checkout{CommitVerification: "strict"},
want: &Checkout{CommitVerification: "warn"},
},
{
name: "remaining fields disjoint",
child: &Checkout{
Expand Down
29 changes: 29 additions & 0 deletions signature/pipeline_invariants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,35 @@ func TestCommandStepWithInvariants_ValuesForFields_WithCheckout(t *testing.T) {
}
}

func TestCommandStepWithInvariants_SignedFields_WithCommitVerification(t *testing.T) {
t.Parallel()

checkout := &pipeline.Checkout{CommitVerification: "strict"}
step := commandStepWithInvariants{
CommandStep: pipeline.CommandStep{
Command: "echo hello",
Plugins: pipeline.Plugins{},
Checkout: checkout,
},
RepositoryURL: "https://github.com/example/repo",
}

fields, err := step.SignedFields()
if err != nil {
t.Fatalf("step.SignedFields() error = %v", err)
}

// checkout should be present since CommitVerification is non-empty.
got, has := fields["checkout"]
if !has {
t.Fatalf("step.SignedFields()[\"checkout\"] absent, want present")
}

if diff := cmp.Diff(got, checkout); diff != "" {
t.Errorf("step.SignedFields()[\"checkout\"] diff (-got +want):\n%s", diff)
}
}

func TestCommandStepWithInvariants_ValuesForFields_MissingCheckoutField(t *testing.T) {
t.Parallel()

Expand Down
103 changes: 103 additions & 0 deletions step_command_checkout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,109 @@ checkout:
}
}

func TestCommandStepWithCommitVerificationYAML(t *testing.T) {
t.Parallel()

yamlData := `
command: echo "hello"
checkout:
commit_verification: strict
`

var step CommandStep
var node yaml.Node

if err := yaml.Unmarshal([]byte(yamlData), &node); err != nil {
t.Fatalf("yaml.Unmarshal() error = %v", err)
}
if err := ordered.Unmarshal(&node, &step); err != nil {
t.Fatalf("ordered.Unmarshal() error = %v", err)
}

if step.Checkout == nil {
t.Fatalf("step.Checkout = nil, want non-nil")
}
if step.Checkout.CommitVerification != "strict" {
t.Errorf("step.Checkout.CommitVerification = %q, want %q", step.Checkout.CommitVerification, "strict")
}
}

func TestCommandStepWithCommitVerificationJSON(t *testing.T) {
t.Parallel()

input := []byte(`{"command":"echo hello","checkout":{"commit_verification":"warn"}}`)

got := new(CommandStep)
if err := got.UnmarshalJSON(input); err != nil {
t.Fatalf("CommandStep.UnmarshalJSON() = %v", err)
}

if got.Checkout == nil {
t.Fatalf("step.Checkout = nil, want non-nil")
}
if got.Checkout.CommitVerification != "warn" {
t.Errorf("step.Checkout.CommitVerification = %q, want %q", got.Checkout.CommitVerification, "warn")
}
}

func TestPipelineCommitVerificationMergeAtBothLevels(t *testing.T) {
t.Parallel()

yamlData := `checkout:
commit_verification: strict
steps:
- command: echo hello
checkout:
commit_verification: warn
- command: echo bye
`

p, err := Parse(strings.NewReader(yamlData))
if err != nil {
t.Fatalf("Parse error = %v", err)
}

if p.Checkout == nil {
t.Fatalf("p.Checkout = nil, want non-nil")
}
if p.Checkout.CommitVerification != "strict" {
t.Errorf("p.Checkout.CommitVerification = %q, want %q", p.Checkout.CommitVerification, "strict")
}

if len(p.Steps) != 2 {
t.Fatalf("len(p.Steps) = %d, want 2", len(p.Steps))
}

// Step 1 has its own commit_verification: warn — should keep it.
step1 := p.Steps[0].(*CommandStep)
if step1.Checkout == nil {
t.Fatalf("step1.Checkout = nil, want non-nil")
}
if step1.Checkout.CommitVerification != "warn" {
t.Errorf("step1.Checkout.CommitVerification = %q, want %q", step1.Checkout.CommitVerification, "warn")
}

// Step 2 has no checkout — should be nil before merge.
step2 := p.Steps[1].(*CommandStep)
if step2.Checkout != nil {
t.Errorf("step2.Checkout = %v, want nil before merge", step2.Checkout)
}

// After merge, step 1 keeps "warn", step 2 inherits "strict".
step1.MergeCheckoutFromPipeline(p.Checkout)
step2.MergeCheckoutFromPipeline(p.Checkout)

if step1.Checkout.CommitVerification != "warn" {
t.Errorf("step1.Checkout.CommitVerification after merge = %q, want %q", step1.Checkout.CommitVerification, "warn")
}
if step2.Checkout == nil {
t.Fatalf("step2.Checkout = nil after merge, want non-nil")
}
if step2.Checkout.CommitVerification != "strict" {
t.Errorf("step2.Checkout.CommitVerification after merge = %q, want %q", step2.Checkout.CommitVerification, "strict")
}
}

func TestCommandStepWithCheckoutJSON(t *testing.T) {
t.Parallel()

Expand Down