diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 36312457..5d8f21d8 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -9749,8 +9749,13 @@ func (t *Pipeline_Workflow) MarshalCBOR(w io.Writer) error { } cw := cbg.NewCborWriter(w) + fieldCount := 5 - if _, err := cw.Write([]byte{164}); err != nil { + if t.RunsOn == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { return err } @@ -9838,6 +9843,42 @@ func (t *Pipeline_Workflow) MarshalCBOR(w io.Writer) error { if _, err := cw.WriteString(string(t.Engine)); err != nil { return err } + + // t.RunsOn ([]string) (slice) + if t.RunsOn != nil { + + if len("runsOn") > 1000000 { + return xerrors.Errorf("Value in field \"runsOn\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("runsOn"))); err != nil { + return err + } + if _, err := cw.WriteString(string("runsOn")); err != nil { + return err + } + + if len(t.RunsOn) > 8192 { + return xerrors.Errorf("Slice value in field t.RunsOn was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.RunsOn))); err != nil { + return err + } + for _, v := range t.RunsOn { + if len(v) > 1000000 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } return nil } @@ -9935,6 +9976,46 @@ func (t *Pipeline_Workflow) UnmarshalCBOR(r io.Reader) (err error) { t.Engine = string(sval) } + // t.RunsOn ([]string) (slice) + case "runsOn": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.RunsOn: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.RunsOn = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.RunsOn[i] = string(sval) + } + + } + } default: // Field doesn't exist on this type, so ignore it diff --git a/api/tangled/tangledpipeline.go b/api/tangled/tangledpipeline.go index da00f406..2151f331 100644 --- a/api/tangled/tangledpipeline.go +++ b/api/tangled/tangledpipeline.go @@ -90,4 +90,5 @@ type Pipeline_Workflow struct { Engine string `json:"engine" cborgen:"engine"` Name string `json:"name" cborgen:"name"` Raw string `json:"raw" cborgen:"raw"` + RunsOn []string `json:"runsOn,omitempty" cborgen:"runsOn,omitempty"` } diff --git a/lexicons/pipeline/pipeline.json b/lexicons/pipeline/pipeline.json index 9deb76fd..3256d172 100644 --- a/lexicons/pipeline/pipeline.json +++ b/lexicons/pipeline/pipeline.json @@ -184,6 +184,12 @@ "engine": { "type": "string" }, + "runsOn": { + "type": "array", + "items": { + "type": "string" + } + }, "clone": { "type": "ref", "ref": "#cloneOpts" diff --git a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs index 04a12079..a274c2a2 100644 --- a/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs +++ b/shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs @@ -2,145 +2,146 @@ // This file is @generated by prost-build. #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Hello { - #[prost(uint32, tag = "1")] + #[prost(uint32, tag="1")] pub protocol_version: u32, - #[prost(string, tag = "2")] + #[prost(string, tag="2")] pub agent_version: ::prost::alloc::string::String, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub boot_id: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub nix_version: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Init { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub job_id: ::prost::alloc::string::String, - #[prost(string, repeated, tag = "2")] + #[prost(string, repeated, tag="2")] pub cache_trusted_public_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(uint32, tag = "3")] + #[prost(uint32, tag="3")] pub cache_read_proxy_port: u32, - #[prost(uint32, tag = "4")] + #[prost(uint32, tag="4")] pub cache_upload_proxy_port: u32, - #[prost(uint32, tag = "5")] + #[prost(uint32, tag="5")] pub dns_proxy_port: u32, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecStart { - #[prost(string, repeated, tag = "1")] + #[prost(string, repeated, tag="1")] pub argv: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, repeated, tag = "2")] + #[prost(string, repeated, tag="2")] pub env: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub cwd: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub user: ::prost::alloc::string::String, - #[prost(uint32, tag = "5")] + #[prost(uint32, tag="5")] pub timeout_seconds: u32, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecStdout { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub data: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecStderr { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub data: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ExecExit { - #[prost(int32, tag = "1")] + #[prost(int32, tag="1")] pub exit_code: i32, - #[prost(string, tag = "2")] + #[prost(string, tag="2")] pub error: ::prost::alloc::string::String, /// set when the guest killed the step on its own timeout timer, so the host /// can classify it as a timeout rather than inferring failure from exit_code. - #[prost(bool, tag = "3")] + #[prost(bool, tag="3")] pub timed_out: bool, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ActivateConfig { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub config_key: ::prost::alloc::string::String, - #[prost(string, tag = "2")] + #[prost(string, tag="2")] pub base_config_hash: ::prost::alloc::string::String, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub user_config: ::prost::alloc::string::String, - #[prost(string, tag = "4")] + #[prost(string, tag="4")] pub toplevel: ::prost::alloc::string::String, - #[prost(uint32, tag = "5")] + #[prost(uint32, tag="5")] pub timeout_seconds: u32, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct ActivateConfigResult { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub config_key: ::prost::alloc::string::String, - #[prost(string, tag = "2")] + #[prost(string, tag="2")] pub toplevel: ::prost::alloc::string::String, - #[prost(string, tag = "3")] + #[prost(string, tag="3")] pub error: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct BuiltPaths { - #[prost(string, repeated, tag = "1")] + #[prost(string, repeated, tag="1")] pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, - #[prost(string, tag = "2")] + #[prost(string, tag="2")] pub reason: ::prost::alloc::string::String, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct CacheDrain { - #[prost(uint32, tag = "1")] + #[prost(uint32, tag="1")] pub timeout_seconds: u32, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct CacheDrainResult { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub error: ::prost::alloc::string::String, - #[prost(uint32, tag = "2")] + #[prost(uint32, tag="2")] pub cache_queued: u32, - #[prost(uint32, tag = "3")] + #[prost(uint32, tag="3")] pub cache_active: u32, - #[prost(uint32, tag = "4")] + #[prost(uint32, tag="4")] pub cache_uploaded: u32, - #[prost(uint32, tag = "5")] + #[prost(uint32, tag="5")] pub cache_failed: u32, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] -pub struct Poweroff {} +pub struct Poweroff { +} #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct PoweroffResult { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub error: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Message { - #[prost(string, tag = "1")] + #[prost(string, tag="1")] pub id: ::prost::alloc::string::String, - #[prost(message, optional, tag = "2")] + #[prost(message, optional, tag="2")] pub hello: ::core::option::Option, - #[prost(message, optional, tag = "3")] + #[prost(message, optional, tag="3")] pub init: ::core::option::Option, - #[prost(message, optional, tag = "4")] + #[prost(message, optional, tag="4")] pub exec_start: ::core::option::Option, - #[prost(message, optional, tag = "5")] + #[prost(message, optional, tag="5")] pub exec_stdout: ::core::option::Option, - #[prost(message, optional, tag = "6")] + #[prost(message, optional, tag="6")] pub exec_stderr: ::core::option::Option, - #[prost(message, optional, tag = "7")] + #[prost(message, optional, tag="7")] pub exec_exit: ::core::option::Option, - #[prost(message, optional, tag = "8")] + #[prost(message, optional, tag="8")] pub activate_config: ::core::option::Option, - #[prost(message, optional, tag = "9")] + #[prost(message, optional, tag="9")] pub activate_config_result: ::core::option::Option, - #[prost(message, optional, tag = "10")] + #[prost(message, optional, tag="10")] pub built_paths: ::core::option::Option, - #[prost(message, optional, tag = "11")] + #[prost(message, optional, tag="11")] pub cache_drain: ::core::option::Option, - #[prost(message, optional, tag = "12")] + #[prost(message, optional, tag="12")] pub cache_drain_result: ::core::option::Option, - #[prost(message, optional, tag = "13")] + #[prost(message, optional, tag="13")] pub poweroff: ::core::option::Option, - #[prost(message, optional, tag = "14")] + #[prost(message, optional, tag="14")] pub poweroff_result: ::core::option::Option, } // @@protoc_insertion_point(module) diff --git a/spindle/engine/manifest_test.go b/spindle/engine/manifest_test.go index cb086bf0..0d71b075 100644 --- a/spindle/engine/manifest_test.go +++ b/spindle/engine/manifest_test.go @@ -79,6 +79,14 @@ func TestDescribeManifestError(t *testing.T) { } } +func TestDescribeManifestErrorAcceptsRunsOnGenericWorkflowKey(t *testing.T) { + raw := "engine: microvm\nruns_on: [linux/arm64, kvm]\nimage: nixos\n" + + if err := DescribeManifestError(raw, testManifest{}); err != nil { + t.Fatalf("DescribeManifestError(%q) = %v, want nil", raw, err) + } +} + func TestDescribeManifestErrorNoFalsePositives(t *testing.T) { cases := []string{ // well-formed manifest diff --git a/workflow/compile.go b/workflow/compile.go index 61bd6e55..d02ff9b9 100644 --- a/workflow/compile.go +++ b/workflow/compile.go @@ -112,8 +112,6 @@ func (compiler *Compiler) Compile(p Pipeline) tangled.Pipeline { } func (compiler *Compiler) compileWorkflow(w Workflow) *tangled.Pipeline_Workflow { - cw := &tangled.Pipeline_Workflow{} - matched, err := w.Match(compiler.Trigger, compiler.ChangedFiles) if err != nil { compiler.Diagnostics.AddError( @@ -134,18 +132,19 @@ func (compiler *Compiler) compileWorkflow(w Workflow) *tangled.Pipeline_Workflow // validate clone options compiler.analyzeCloneOptions(w) - cw.Name = w.Name - if w.Engine == "" { compiler.Diagnostics.AddError(w.Name, MissingEngine) return nil } - cw.Engine = w.Engine - cw.Raw = w.Raw - o := w.CloneOpts.AsRecord() - cw.Clone = &o + cw := &tangled.Pipeline_Workflow{ + Clone: &o, + Engine: w.Engine, + Name: w.Name, + Raw: w.Raw, + RunsOn: w.RunsOn, + } return cw } diff --git a/workflow/compile_test.go b/workflow/compile_test.go index 45428552..a8354ad7 100644 --- a/workflow/compile_test.go +++ b/workflow/compile_test.go @@ -1,6 +1,7 @@ package workflow import ( + "encoding/json" "strings" "testing" @@ -41,6 +42,50 @@ func TestCompileWorkflow_MatchingWorkflowWithSteps(t *testing.T) { assert.False(t, c.Diagnostics.IsErr()) } +func TestCompileWorkflow_RunsOnLabelsPersistWithoutFanout(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/arm64.yml", + Engine: "microvm", + When: when, + RunsOn: []string{"linux/arm64", "kvm"}, + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 1) + assert.Equal(t, []string{"linux/arm64", "kvm"}, cp.Workflows[0].RunsOn) + + raw, err := json.Marshal(cp.Workflows[0]) + assert.NoError(t, err) + + var persisted map[string]any + assert.NoError(t, json.Unmarshal(raw, &persisted)) + assert.Equal(t, []any{"linux/arm64", "kvm"}, persisted["runsOn"]) +} + +func TestCompileWorkflow_LegacyWorkflowOmitsRunsOn(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/legacy.yml", + Engine: "microvm", + When: when, + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 1) + assert.Empty(t, cp.Workflows[0].RunsOn) + + raw, err := json.Marshal(cp.Workflows[0]) + assert.NoError(t, err) + + var persisted map[string]any + assert.NoError(t, json.Unmarshal(raw, &persisted)) + _, ok := persisted["runsOn"] + assert.False(t, ok, "legacy workflow JSON should not gain runsOn") +} + func TestCompileWorkflow_TriggerMismatch(t *testing.T) { wf := Workflow{ Name: ".tangled/workflows/mismatch.yml", diff --git a/workflow/def.go b/workflow/def.go index ddd9b7d0..055fc043 100644 --- a/workflow/def.go +++ b/workflow/def.go @@ -27,6 +27,7 @@ type ( Workflow struct { Name string `yaml:"-"` // name of the workflow file Engine string `yaml:"engine"` + RunsOn []string `yaml:"runs_on"` When []Constraint `yaml:"when"` CloneOpts CloneOpts `yaml:"clone"` Raw string `yaml:"-"` @@ -121,15 +122,15 @@ func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles } func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) { - match := true - // manual triggers always pass this constraint if trigger.Manual != nil { return true, nil } // apply event constraints - match = match && c.MatchEvent(trigger.Kind) + if !c.MatchEvent(trigger.Kind) { + return false, nil + } // apply branch constraints for PRs if trigger.PullRequest != nil { @@ -137,7 +138,9 @@ func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFile if err != nil { return false, err } - match = match && matched + if !matched { + return false, nil + } } // apply ref constraints for pushes @@ -146,7 +149,9 @@ func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFile if err != nil { return false, err } - match = match && matched + if !matched { + return false, nil + } } // apply paths filter: if specified, at least one changed file must match @@ -155,10 +160,12 @@ func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFile if err != nil { return false, err } - match = match && matched + if !matched { + return false, nil + } } - return match, nil + return true, nil } // matchesAnyFile returns true if any file in files matches any of the glob patterns. diff --git a/workflow/def_test.go b/workflow/def_test.go index 6f246f9c..165115d8 100644 --- a/workflow/def_test.go +++ b/workflow/def_test.go @@ -23,6 +23,19 @@ when: assert.False(t, wf.CloneOpts.Skip, "Skip should default to false") } +func TestUnmarshalWorkflowWithRunsOnLabels(t *testing.T) { + yamlData := ` +engine: microvm +runs_on: [linux/arm64, kvm] +when: + - event: push` + + wf, err := FromFile("test.yml", []byte(yamlData)) + assert.NoError(t, err) + + assert.Equal(t, []string{"linux/arm64", "kvm"}, wf.RunsOn) +} + func TestUnmarshalCloneFalse(t *testing.T) { yamlData := ` when: