Class: Bsdkrun::CIWorkflow
- Inherits:
-
Object
- Object
- Bsdkrun::CIWorkflow
- Defined in:
- lib/bsdkrun/ci.rb
Overview
CI workflows defined in code instead of YAML.
The builder produces exactly the file bsdkrun ci (and tangled's spindle)
consumes — #yaml is that file, #save commits it to
.tangled/workflows/, and #run executes it in a microVM
without a file ever touching the repository:
Bsdkrun.workflow("test")
.on_push("main")
.deps("ruby", "bundler")
.env("CI_FROM", "sdk")
.step("install", "bundle install")
.step("test", "bundle exec rspec")
.run
Code is the source of truth and YAML the wire format, in that order —
which is why save writes a generated-file header: a hand-edit there will
be overwritten by the next save.
Instance Method Summary collapse
-
#clone_depth(depth) ⇒ Object
Set the clone depth (default 1).
-
#deps(*packages) ⇒ Object
Add nixpkgs dependencies — the toolchain the steps run against.
-
#deps_from(registry, *packages) ⇒ Object
Add dependencies from a custom registry (a flake reference).
-
#engine(engine) ⇒ Object
Override the engine (+nixery+ by default).
-
#env(key, value) ⇒ Object
Set a workflow-level environment variable.
-
#file_name ⇒ Object
The workflow file name #save writes:
<name>.yml. -
#initialize(name) ⇒ CIWorkflow
constructor
A new instance of CIWorkflow.
-
#on(events, *branches) ⇒ Object
Add a trigger with explicit events.
-
#on_pull_request(*branches) ⇒ Object
Add a pull_request trigger targeting the given branches.
-
#on_push(*branches) ⇒ Object
Add a push trigger for the given branches.
-
#run(dir: nil) ⇒ Object
Execute the workflow in a microVM, streaming output.
-
#save(repo) ⇒ Object
Write into
<repo>/.tangled/workflows/and return the path. -
#skip_clone ⇒ Object
Skip the checkout entirely.
-
#step(name, command, env: nil) ⇒ Object
Append a step; steps run serially in one VM, from the workspace root.
-
#yaml ⇒ Object
Render the workflow file.
Constructor Details
#initialize(name) ⇒ CIWorkflow
Returns a new instance of CIWorkflow.
27 28 29 30 31 32 33 34 35 36 |
# File 'lib/bsdkrun/ci.rb', line 27 def initialize(name) @name = name @engine = "nixery" @when = [] @deps = {} @env = {} @steps = [] @clone_depth = nil @clone_skip = false end |
Instance Method Details
#clone_depth(depth) ⇒ Object
Set the clone depth (default 1).
87 88 89 90 |
# File 'lib/bsdkrun/ci.rb', line 87 def clone_depth(depth) @clone_depth = depth self end |
#deps(*packages) ⇒ Object
Add nixpkgs dependencies — the toolchain the steps run against.
63 64 65 66 |
# File 'lib/bsdkrun/ci.rb', line 63 def deps(*packages) (@deps["nixpkgs"] ||= []).concat(packages) self end |
#deps_from(registry, *packages) ⇒ Object
Add dependencies from a custom registry (a flake reference).
69 70 71 72 |
# File 'lib/bsdkrun/ci.rb', line 69 def deps_from(registry, *packages) (@deps[registry] ||= []).concat(packages) self end |
#engine(engine) ⇒ Object
Override the engine (+nixery+ by default).
39 40 41 42 |
# File 'lib/bsdkrun/ci.rb', line 39 def engine(engine) @engine = engine self end |
#env(key, value) ⇒ Object
Set a workflow-level environment variable.
75 76 77 78 |
# File 'lib/bsdkrun/ci.rb', line 75 def env(key, value) @env[key] = value self end |
#file_name ⇒ Object
The workflow file name #save writes: <name>.yml.
99 100 101 |
# File 'lib/bsdkrun/ci.rb', line 99 def file_name @name.match?(/\.ya?ml\z/) ? @name : "#{@name}.yml" end |
#on(events, *branches) ⇒ Object
Add a trigger with explicit events.
57 58 59 60 |
# File 'lib/bsdkrun/ci.rb', line 57 def on(events, *branches) @when << [events, branches] self end |
#on_pull_request(*branches) ⇒ Object
Add a pull_request trigger targeting the given branches.
51 52 53 54 |
# File 'lib/bsdkrun/ci.rb', line 51 def on_pull_request(*branches) @when << [["pull_request"], branches] self end |
#on_push(*branches) ⇒ Object
Add a push trigger for the given branches.
45 46 47 48 |
# File 'lib/bsdkrun/ci.rb', line 45 def on_push(*branches) @when << [["push"], branches] self end |
#run(dir: nil) ⇒ Object
Execute the workflow in a microVM, streaming output. The YAML never
touches the repository — it goes to a temp file and bsdkrun ci run -f.
Raises Bsdkrun::CommandFailed when a step fails.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/bsdkrun/ci.rb', line 182 def run(dir: nil) Dir.mktmpdir("bsdkrun-ci-") do |tmp| file = File.join(tmp, file_name) File.write(file, yaml) args = ["ci", "run", "-f", file] args += ["-w", dir] if dir ok = Process.spawn_interactive(args) unless ok raise CommandFailed.new( exit_code: 1, stdout: "", stderr: "workflow #{@name} failed", command: "bsdkrun ci run" ) end end nil end |
#save(repo) ⇒ Object
Write into <repo>/.tangled/workflows/ and return the path.
168 169 170 171 172 173 174 175 176 177 |
# File 'lib/bsdkrun/ci.rb', line 168 def save(repo) dir = File.join(repo, ".tangled", "workflows") FileUtils.mkdir_p(dir) path = File.join(dir, file_name) File.write( path, "# Generated by the bsdkrun SDK — edit the code that save()d it instead.\n#{yaml}" ) path end |
#skip_clone ⇒ Object
Skip the checkout entirely.
93 94 95 96 |
# File 'lib/bsdkrun/ci.rb', line 93 def skip_clone @clone_skip = true self end |
#step(name, command, env: nil) ⇒ Object
Append a step; steps run serially in one VM, from the workspace root.
81 82 83 84 |
# File 'lib/bsdkrun/ci.rb', line 81 def step(name, command, env: nil) @steps << { name: name, command: command, env: env || {} } self end |
#yaml ⇒ Object
Render the workflow file. Scalars are emitted as JSON strings — valid YAML by construction — and commands as literal blocks when safe, so the SDK needs no YAML dependency.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/bsdkrun/ci.rb', line 106 def yaml out = [] q = ->(s) { JSON.generate(s) } unless @when.empty? out << "when:" @when.each do |events, branches| out << " - event: [#{events.map(&q).join(', ')}]" if branches.length == 1 out << " branch: #{q.call(branches[0])}" elsif branches.length > 1 out << " branch: [#{branches.map(&q).join(', ')}]" end end out << "" end out << "engine: #{@engine}" unless @deps.empty? out << "" << "dependencies:" @deps.keys.sort.each do |reg| out << " #{q.call(reg)}:" @deps[reg].each { |p| out << " - #{q.call(p)}" } end end unless @env.empty? out << "" << "environment:" @env.keys.sort.each { |k| out << " #{k}: #{q.call(@env[k])}" } end if @clone_skip || @clone_depth out << "" << "clone:" out << " skip: true" if @clone_skip out << " depth: #{@clone_depth}" if @clone_depth end out << "" << "steps:" @steps.each do |s| out << " - name: #{q.call(s[:name])}" # Literal blocks read well in a committed file, but cannot carry # trailing spaces or carriage returns byte-for-byte; fall back to a # JSON string rather than silently altering the command. block_safe = !s[:command].empty? && !s[:command].include?("\r") && s[:command].split("\n", -1).all? { |l| l == l.sub(/ +\z/, "") } if block_safe out << " command: |" s[:command].sub(/\n+\z/, "").split("\n", -1).each { |l| out << " #{l}" } else out << " command: #{q.call(s[:command])}" end next if s[:env].empty? out << " environment:" s[:env].keys.sort.each { |k| out << " #{k}: #{q.call(s[:env][k])}" } end "#{out.join("\n")}\n" end |