Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

workflow: add runs_on label matching

dawn (Jul 13, 2026, 4:22 PM +0300) a73330d1 eade059d

+228 -67
+82 -1
api/tangled/cbor_gen.go
··· 9749 9749 } 9750 9750 9751 9751 cw := cbg.NewCborWriter(w) 9752 + fieldCount := 5 9752 9753 9753 - if _, err := cw.Write([]byte{164}); err != nil { 9754 + if t.RunsOn == nil { 9755 + fieldCount-- 9756 + } 9757 + 9758 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 9754 9759 return err 9755 9760 } 9756 9761 ··· 9838 9843 if _, err := cw.WriteString(string(t.Engine)); err != nil { 9839 9844 return err 9840 9845 } 9846 + 9847 + // t.RunsOn ([]string) (slice) 9848 + if t.RunsOn != nil { 9849 + 9850 + if len("runsOn") > 1000000 { 9851 + return xerrors.Errorf("Value in field \"runsOn\" was too long") 9852 + } 9853 + 9854 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("runsOn"))); err != nil { 9855 + return err 9856 + } 9857 + if _, err := cw.WriteString(string("runsOn")); err != nil { 9858 + return err 9859 + } 9860 + 9861 + if len(t.RunsOn) > 8192 { 9862 + return xerrors.Errorf("Slice value in field t.RunsOn was too long") 9863 + } 9864 + 9865 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.RunsOn))); err != nil { 9866 + return err 9867 + } 9868 + for _, v := range t.RunsOn { 9869 + if len(v) > 1000000 { 9870 + return xerrors.Errorf("Value in field v was too long") 9871 + } 9872 + 9873 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 9874 + return err 9875 + } 9876 + if _, err := cw.WriteString(string(v)); err != nil { 9877 + return err 9878 + } 9879 + 9880 + } 9881 + } 9841 9882 return nil 9842 9883 } 9843 9884 ··· 9934 9975 } 9935 9976 9936 9977 t.Engine = string(sval) 9978 + } 9979 + // t.RunsOn ([]string) (slice) 9980 + case "runsOn": 9981 + 9982 + maj, extra, err = cr.ReadHeader() 9983 + if err != nil { 9984 + return err 9985 + } 9986 + 9987 + if extra > 8192 { 9988 + return fmt.Errorf("t.RunsOn: array too large (%d)", extra) 9989 + } 9990 + 9991 + if maj != cbg.MajArray { 9992 + return fmt.Errorf("expected cbor array") 9993 + } 9994 + 9995 + if extra > 0 { 9996 + t.RunsOn = make([]string, extra) 9997 + } 9998 + 9999 + for i := 0; i < int(extra); i++ { 10000 + { 10001 + var maj byte 10002 + var extra uint64 10003 + var err error 10004 + _ = maj 10005 + _ = extra 10006 + _ = err 10007 + 10008 + { 10009 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 10010 + if err != nil { 10011 + return err 10012 + } 10013 + 10014 + t.RunsOn[i] = string(sval) 10015 + } 10016 + 10017 + } 9937 10018 } 9938 10019 9939 10020 default:
+1
api/tangled/tangledpipeline.go
··· 90 90 Engine string `json:"engine" cborgen:"engine"` 91 91 Name string `json:"name" cborgen:"name"` 92 92 Raw string `json:"raw" cborgen:"raw"` 93 + RunsOn []string `json:"runsOn,omitempty" cborgen:"runsOn,omitempty"` 93 94 }
+6
lexicons/pipeline/pipeline.json
··· 184 184 "engine": { 185 185 "type": "string" 186 186 }, 187 + "runsOn": { 188 + "type": "array", 189 + "items": { 190 + "type": "string" 191 + } 192 + }, 187 193 "clone": { 188 194 "type": "ref", 189 195 "ref": "#cloneOpts"
+52 -51
shuttle/src/gen/spindle/agent/v1/spindle.agent.v1.rs
··· 2 2 // This file is @generated by prost-build. 3 3 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 4 4 pub struct Hello { 5 - #[prost(uint32, tag = "1")] 5 + #[prost(uint32, tag="1")] 6 6 pub protocol_version: u32, 7 - #[prost(string, tag = "2")] 7 + #[prost(string, tag="2")] 8 8 pub agent_version: ::prost::alloc::string::String, 9 - #[prost(string, tag = "3")] 9 + #[prost(string, tag="3")] 10 10 pub boot_id: ::prost::alloc::string::String, 11 - #[prost(string, tag = "4")] 11 + #[prost(string, tag="4")] 12 12 pub nix_version: ::prost::alloc::string::String, 13 13 } 14 14 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 15 15 pub struct Init { 16 - #[prost(string, tag = "1")] 16 + #[prost(string, tag="1")] 17 17 pub job_id: ::prost::alloc::string::String, 18 - #[prost(string, repeated, tag = "2")] 18 + #[prost(string, repeated, tag="2")] 19 19 pub cache_trusted_public_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 20 - #[prost(uint32, tag = "3")] 20 + #[prost(uint32, tag="3")] 21 21 pub cache_read_proxy_port: u32, 22 - #[prost(uint32, tag = "4")] 22 + #[prost(uint32, tag="4")] 23 23 pub cache_upload_proxy_port: u32, 24 - #[prost(uint32, tag = "5")] 24 + #[prost(uint32, tag="5")] 25 25 pub dns_proxy_port: u32, 26 26 } 27 27 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 28 28 pub struct ExecStart { 29 - #[prost(string, repeated, tag = "1")] 29 + #[prost(string, repeated, tag="1")] 30 30 pub argv: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 31 - #[prost(string, repeated, tag = "2")] 31 + #[prost(string, repeated, tag="2")] 32 32 pub env: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 33 - #[prost(string, tag = "3")] 33 + #[prost(string, tag="3")] 34 34 pub cwd: ::prost::alloc::string::String, 35 - #[prost(string, tag = "4")] 35 + #[prost(string, tag="4")] 36 36 pub user: ::prost::alloc::string::String, 37 - #[prost(uint32, tag = "5")] 37 + #[prost(uint32, tag="5")] 38 38 pub timeout_seconds: u32, 39 39 } 40 40 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 41 41 pub struct ExecStdout { 42 - #[prost(string, tag = "1")] 42 + #[prost(string, tag="1")] 43 43 pub data: ::prost::alloc::string::String, 44 44 } 45 45 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 46 46 pub struct ExecStderr { 47 - #[prost(string, tag = "1")] 47 + #[prost(string, tag="1")] 48 48 pub data: ::prost::alloc::string::String, 49 49 } 50 50 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 51 51 pub struct ExecExit { 52 - #[prost(int32, tag = "1")] 52 + #[prost(int32, tag="1")] 53 53 pub exit_code: i32, 54 - #[prost(string, tag = "2")] 54 + #[prost(string, tag="2")] 55 55 pub error: ::prost::alloc::string::String, 56 56 /// set when the guest killed the step on its own timeout timer, so the host 57 57 /// can classify it as a timeout rather than inferring failure from exit_code. 58 - #[prost(bool, tag = "3")] 58 + #[prost(bool, tag="3")] 59 59 pub timed_out: bool, 60 60 } 61 61 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 62 62 pub struct ActivateConfig { 63 - #[prost(string, tag = "1")] 63 + #[prost(string, tag="1")] 64 64 pub config_key: ::prost::alloc::string::String, 65 - #[prost(string, tag = "2")] 65 + #[prost(string, tag="2")] 66 66 pub base_config_hash: ::prost::alloc::string::String, 67 - #[prost(string, tag = "3")] 67 + #[prost(string, tag="3")] 68 68 pub user_config: ::prost::alloc::string::String, 69 - #[prost(string, tag = "4")] 69 + #[prost(string, tag="4")] 70 70 pub toplevel: ::prost::alloc::string::String, 71 - #[prost(uint32, tag = "5")] 71 + #[prost(uint32, tag="5")] 72 72 pub timeout_seconds: u32, 73 73 } 74 74 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 75 75 pub struct ActivateConfigResult { 76 - #[prost(string, tag = "1")] 76 + #[prost(string, tag="1")] 77 77 pub config_key: ::prost::alloc::string::String, 78 - #[prost(string, tag = "2")] 78 + #[prost(string, tag="2")] 79 79 pub toplevel: ::prost::alloc::string::String, 80 - #[prost(string, tag = "3")] 80 + #[prost(string, tag="3")] 81 81 pub error: ::prost::alloc::string::String, 82 82 } 83 83 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 84 84 pub struct BuiltPaths { 85 - #[prost(string, repeated, tag = "1")] 85 + #[prost(string, repeated, tag="1")] 86 86 pub paths: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 87 - #[prost(string, tag = "2")] 87 + #[prost(string, tag="2")] 88 88 pub reason: ::prost::alloc::string::String, 89 89 } 90 90 #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] 91 91 pub struct CacheDrain { 92 - #[prost(uint32, tag = "1")] 92 + #[prost(uint32, tag="1")] 93 93 pub timeout_seconds: u32, 94 94 } 95 95 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 96 96 pub struct CacheDrainResult { 97 - #[prost(string, tag = "1")] 97 + #[prost(string, tag="1")] 98 98 pub error: ::prost::alloc::string::String, 99 - #[prost(uint32, tag = "2")] 99 + #[prost(uint32, tag="2")] 100 100 pub cache_queued: u32, 101 - #[prost(uint32, tag = "3")] 101 + #[prost(uint32, tag="3")] 102 102 pub cache_active: u32, 103 - #[prost(uint32, tag = "4")] 103 + #[prost(uint32, tag="4")] 104 104 pub cache_uploaded: u32, 105 - #[prost(uint32, tag = "5")] 105 + #[prost(uint32, tag="5")] 106 106 pub cache_failed: u32, 107 107 } 108 108 #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] 109 - pub struct Poweroff {} 109 + pub struct Poweroff { 110 + } 110 111 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 111 112 pub struct PoweroffResult { 112 - #[prost(string, tag = "1")] 113 + #[prost(string, tag="1")] 113 114 pub error: ::prost::alloc::string::String, 114 115 } 115 116 #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] 116 117 pub struct Message { 117 - #[prost(string, tag = "1")] 118 + #[prost(string, tag="1")] 118 119 pub id: ::prost::alloc::string::String, 119 - #[prost(message, optional, tag = "2")] 120 + #[prost(message, optional, tag="2")] 120 121 pub hello: ::core::option::Option<Hello>, 121 - #[prost(message, optional, tag = "3")] 122 + #[prost(message, optional, tag="3")] 122 123 pub init: ::core::option::Option<Init>, 123 - #[prost(message, optional, tag = "4")] 124 + #[prost(message, optional, tag="4")] 124 125 pub exec_start: ::core::option::Option<ExecStart>, 125 - #[prost(message, optional, tag = "5")] 126 + #[prost(message, optional, tag="5")] 126 127 pub exec_stdout: ::core::option::Option<ExecStdout>, 127 - #[prost(message, optional, tag = "6")] 128 + #[prost(message, optional, tag="6")] 128 129 pub exec_stderr: ::core::option::Option<ExecStderr>, 129 - #[prost(message, optional, tag = "7")] 130 + #[prost(message, optional, tag="7")] 130 131 pub exec_exit: ::core::option::Option<ExecExit>, 131 - #[prost(message, optional, tag = "8")] 132 + #[prost(message, optional, tag="8")] 132 133 pub activate_config: ::core::option::Option<ActivateConfig>, 133 - #[prost(message, optional, tag = "9")] 134 + #[prost(message, optional, tag="9")] 134 135 pub activate_config_result: ::core::option::Option<ActivateConfigResult>, 135 - #[prost(message, optional, tag = "10")] 136 + #[prost(message, optional, tag="10")] 136 137 pub built_paths: ::core::option::Option<BuiltPaths>, 137 - #[prost(message, optional, tag = "11")] 138 + #[prost(message, optional, tag="11")] 138 139 pub cache_drain: ::core::option::Option<CacheDrain>, 139 - #[prost(message, optional, tag = "12")] 140 + #[prost(message, optional, tag="12")] 140 141 pub cache_drain_result: ::core::option::Option<CacheDrainResult>, 141 - #[prost(message, optional, tag = "13")] 142 + #[prost(message, optional, tag="13")] 142 143 pub poweroff: ::core::option::Option<Poweroff>, 143 - #[prost(message, optional, tag = "14")] 144 + #[prost(message, optional, tag="14")] 144 145 pub poweroff_result: ::core::option::Option<PoweroffResult>, 145 146 } 146 147 // @@protoc_insertion_point(module)
+8
spindle/engine/manifest_test.go
··· 79 79 } 80 80 } 81 81 82 + func TestDescribeManifestErrorAcceptsRunsOnGenericWorkflowKey(t *testing.T) { 83 + raw := "engine: microvm\nruns_on: [linux/arm64, kvm]\nimage: nixos\n" 84 + 85 + if err := DescribeManifestError(raw, testManifest{}); err != nil { 86 + t.Fatalf("DescribeManifestError(%q) = %v, want nil", raw, err) 87 + } 88 + } 89 + 82 90 func TestDescribeManifestErrorNoFalsePositives(t *testing.T) { 83 91 cases := []string{ 84 92 // well-formed manifest
+7 -8
workflow/compile.go
··· 112 112 } 113 113 114 114 func (compiler *Compiler) compileWorkflow(w Workflow) *tangled.Pipeline_Workflow { 115 - cw := &tangled.Pipeline_Workflow{} 116 - 117 115 matched, err := w.Match(compiler.Trigger, compiler.ChangedFiles) 118 116 if err != nil { 119 117 compiler.Diagnostics.AddError( ··· 134 132 // validate clone options 135 133 compiler.analyzeCloneOptions(w) 136 134 137 - cw.Name = w.Name 138 - 139 135 if w.Engine == "" { 140 136 compiler.Diagnostics.AddError(w.Name, MissingEngine) 141 137 return nil 142 138 } 143 139 144 - cw.Engine = w.Engine 145 - cw.Raw = w.Raw 146 - 147 140 o := w.CloneOpts.AsRecord() 148 - cw.Clone = &o 141 + cw := &tangled.Pipeline_Workflow{ 142 + Clone: &o, 143 + Engine: w.Engine, 144 + Name: w.Name, 145 + Raw: w.Raw, 146 + RunsOn: w.RunsOn, 147 + } 149 148 150 149 return cw 151 150 }
+45
workflow/compile_test.go
··· 1 1 package workflow 2 2 3 3 import ( 4 + "encoding/json" 4 5 "strings" 5 6 "testing" 6 7 ··· 39 40 assert.Equal(t, wf.Name, cp.Workflows[0].Name) 40 41 assert.False(t, cp.Workflows[0].Clone.Skip) 41 42 assert.False(t, c.Diagnostics.IsErr()) 43 + } 44 + 45 + func TestCompileWorkflow_RunsOnLabelsPersistWithoutFanout(t *testing.T) { 46 + wf := Workflow{ 47 + Name: ".tangled/workflows/arm64.yml", 48 + Engine: "microvm", 49 + When: when, 50 + RunsOn: []string{"linux/arm64", "kvm"}, 51 + } 52 + 53 + c := Compiler{Trigger: trigger} 54 + cp := c.Compile([]Workflow{wf}) 55 + 56 + assert.Len(t, cp.Workflows, 1) 57 + assert.Equal(t, []string{"linux/arm64", "kvm"}, cp.Workflows[0].RunsOn) 58 + 59 + raw, err := json.Marshal(cp.Workflows[0]) 60 + assert.NoError(t, err) 61 + 62 + var persisted map[string]any 63 + assert.NoError(t, json.Unmarshal(raw, &persisted)) 64 + assert.Equal(t, []any{"linux/arm64", "kvm"}, persisted["runsOn"]) 65 + } 66 + 67 + func TestCompileWorkflow_LegacyWorkflowOmitsRunsOn(t *testing.T) { 68 + wf := Workflow{ 69 + Name: ".tangled/workflows/legacy.yml", 70 + Engine: "microvm", 71 + When: when, 72 + } 73 + 74 + c := Compiler{Trigger: trigger} 75 + cp := c.Compile([]Workflow{wf}) 76 + 77 + assert.Len(t, cp.Workflows, 1) 78 + assert.Empty(t, cp.Workflows[0].RunsOn) 79 + 80 + raw, err := json.Marshal(cp.Workflows[0]) 81 + assert.NoError(t, err) 82 + 83 + var persisted map[string]any 84 + assert.NoError(t, json.Unmarshal(raw, &persisted)) 85 + _, ok := persisted["runsOn"] 86 + assert.False(t, ok, "legacy workflow JSON should not gain runsOn") 42 87 } 43 88 44 89 func TestCompileWorkflow_TriggerMismatch(t *testing.T) {
+14 -7
workflow/def.go
··· 27 27 Workflow struct { 28 28 Name string `yaml:"-"` // name of the workflow file 29 29 Engine string `yaml:"engine"` 30 + RunsOn []string `yaml:"runs_on"` 30 31 When []Constraint `yaml:"when"` 31 32 CloneOpts CloneOpts `yaml:"clone"` 32 33 Raw string `yaml:"-"` ··· 121 122 } 122 123 123 124 func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) { 124 - match := true 125 - 126 125 // manual triggers always pass this constraint 127 126 if trigger.Manual != nil { 128 127 return true, nil 129 128 } 130 129 131 130 // apply event constraints 132 - match = match && c.MatchEvent(trigger.Kind) 131 + if !c.MatchEvent(trigger.Kind) { 132 + return false, nil 133 + } 133 134 134 135 // apply branch constraints for PRs 135 136 if trigger.PullRequest != nil { ··· 137 138 if err != nil { 138 139 return false, err 139 140 } 140 - match = match && matched 141 + if !matched { 142 + return false, nil 143 + } 141 144 } 142 145 143 146 // apply ref constraints for pushes ··· 146 149 if err != nil { 147 150 return false, err 148 151 } 149 - match = match && matched 152 + if !matched { 153 + return false, nil 154 + } 150 155 } 151 156 152 157 // apply paths filter: if specified, at least one changed file must match ··· 155 160 if err != nil { 156 161 return false, err 157 162 } 158 - match = match && matched 163 + if !matched { 164 + return false, nil 165 + } 159 166 } 160 167 161 - return match, nil 168 + return true, nil 162 169 } 163 170 164 171 // matchesAnyFile returns true if any file in files matches any of the glob patterns.
+13
workflow/def_test.go
··· 23 23 assert.False(t, wf.CloneOpts.Skip, "Skip should default to false") 24 24 } 25 25 26 + func TestUnmarshalWorkflowWithRunsOnLabels(t *testing.T) { 27 + yamlData := ` 28 + engine: microvm 29 + runs_on: [linux/arm64, kvm] 30 + when: 31 + - event: push` 32 + 33 + wf, err := FromFile("test.yml", []byte(yamlData)) 34 + assert.NoError(t, err) 35 + 36 + assert.Equal(t, []string{"linux/arm64", "kvm"}, wf.RunsOn) 37 + } 38 + 26 39 func TestUnmarshalCloneFalse(t *testing.T) { 27 40 yamlData := ` 28 41 when: