Commit

Author:

Hash:

Timestamp:

+217 -107 +/-16 browse

Kevin Schoon [me@kevinschoon.com]

f37d02f238847605fe3ef17ac01986df998f1bd7

Fri, 12 Jun 2026 13:11:15 +0000 (1 month ago)

various build related cleanup
various build related cleanup

Adds a --debug flag to ayllu-build evaluate to pause the build on
failures and allow the user to attach to the session in another terminal.

Cleaned up Ayllu's own CI a bit eliminated many of the useless outputs
and it now only produces a single Arch package.
1diff --git a/.ayllu-build.jsonnet b/.ayllu-build.jsonnet
2index 1057b21..fa4e9ff 100644
3--- a/.ayllu-build.jsonnet
4+++ b/.ayllu-build.jsonnet
5 @@ -2,6 +2,7 @@ local target = 'x86_64-alpine-linux-musl';
6 local build_image = 'ayllu-arch';
7 local version = '0.5.1';
8 local arch_package = 'ayllu-dev-' + version + '-1-x86_64.pkg.tar.zst';
9+ local cargo_target_dir = '/home/ayllu/cargo-target';
10
11 local cargo_cache = {
12 name: 'cargo-cache',
13 @@ -10,12 +11,11 @@ local cargo_cache = {
14
15 local build_cache = {
16 name: 'build-cache',
17- path: '/src/target',
18+ path: '/home/ayllu/cargo-target',
19 transient: true,
20 };
21
22 local binaries = [
23- 'ayllu-init',
24 'ayllu-build',
25 'ayllu-keys',
26 'ayllu-migrate',
27 @@ -32,7 +32,11 @@ local subcommands = [
28
29 local compile_binary(name) = {
30 name: 'Cargo Build ' + name,
31- input: 'cargo build --release --bin=' + name,
32+ input: 'cargo build --frozen --release --bin=' + name,
33+ environment: {
34+ AYLLU_VERSION: version,
35+ CARGO_TARGET_DIR: cargo_target_dir,
36+ }
37 };
38
39 local zsh_complete(name) = {
40 @@ -53,6 +57,10 @@ local man_page(name, section) = {
41 local short_step(name, input) = {
42 name: name,
43 input: input,
44+ environment: {
45+ AYLLU_VERSION: version,
46+ CARGO_TARGET_DIR: cargo_target_dir,
47+ },
48 };
49
50 {
51 @@ -66,53 +74,64 @@ local short_step(name, input) = {
52 cache: [cargo_cache, build_cache],
53 },
54 {
55- name: 'Distribution',
56+ name: 'Metadata',
57+ image: build_image,
58+ networking: true,
59+ steps: [
60+ short_step('Fetch', 'cargo fetch --locked'),
61+ short_step('Metadata', 'cargo metadata > metadata.json'),
62+ ],
63+ cache: [cargo_cache, build_cache],
64+ outputs: [{
65+ path: 'metadata.json',
66+ sha256sum: null,
67+ }],
68 depends_on: ['Lint'],
69+ },
70+ {
71+ name: 'Test',
72 image: build_image,
73+ steps: [
74+ short_step('Cargo Test', 'cargo test --frozen --release'),
75+ ],
76 cache: [cargo_cache, build_cache],
77- steps:
78- std.flattenArrays(
79- [
80- [
81- {
82- name: 'Man',
83- input: 'cargo run --package xtask -- gen_man',
84- },
85- {
86- name: 'Completion',
87- input: 'cargo run --package xtask -- complete',
88- },
89- ],
90- [
91- short_step('Cargo Test', 'cargo test'),
92- short_step('Cargo Clippy', 'cargo clippy'),
93- ],
94- [
95- compile_binary(binary)
96- for binary in binaries
97- ],
98- [
99- {
100- name: 'Arch Package',
101- input: 'VERSION=' + version + ' makepkg -f',
102- },
103- ],
104- ]
105- ),
106- outputs: std.flattenArrays([
107- [zsh_complete(name) for name in binaries],
108- [bash_complete(name) for name in binaries],
109- [man_page(name, '1') for name in binaries],
110- [man_page('ayllu', '7')],
111- [{ path: arch_package }],
112+ depends_on: ['Lint'],
113+ },
114+ {
115+ name: 'Compile Binaries',
116+ image: build_image,
117+ steps: std.flattenArrays(
118 [
119- {
120- path: 'target/release/' + binary,
121- sha256sum: null,
122- }
123- for binary in binaries
124- ],
125- ]),
126+ [
127+ // NOTE: Somewhat oddly man pages are embedded in the ayllu-web binary
128+ // and so this task must always be ran before compiling ayllu-web.
129+ short_step('Man Pages', 'cargo run --frozen --package xtask -- gen_man'),
130+ ],
131+ [
132+ compile_binary(binary)
133+ for binary in binaries
134+ ],
135+ ]
136+ ),
137+ cache: [cargo_cache, build_cache],
138+ depends_on: ['Test'],
139 },
140+ {
141+ name: 'Arch Distribution',
142+ image: build_image,
143+ steps: [
144+ short_step('Man Pages', 'cargo run --frozen --package xtask -- gen_man'),
145+ short_step('Shell Completion', 'cargo run --frozen --package xtask -- complete'),
146+ short_step('Makepkg', 'makepkg')
147+ ],
148+ cache: [cargo_cache, build_cache],
149+ depends_on: ['Compile Binaries'],
150+ outputs: [
151+ {
152+ path: arch_package,
153+ sha256sum: null
154+ }
155+ ]
156+ }
157 ],
158 }
159 diff --git a/.gitignore b/.gitignore
160index c1615dd..0f589be 100644
161--- a/.gitignore
162+++ b/.gitignore
163 @@ -17,3 +17,7 @@ demo
164 *.zst
165 /src
166 pkg
167+
168+ dist/completion/*
169+ dist/man/*
170+ !dist/man/.gitkeep
171 diff --git a/PKGBUILD b/PKGBUILD
172index 4d58aa1..580281e 100644
173--- a/PKGBUILD
174+++ b/PKGBUILD
175 @@ -1,14 +1,22 @@
176+ # This is the PKGBUILD for Ayllu's internal build server. Upstream arch builds
177+ # should be managed seperately on the AUR but can use this file as a reference.
178+ #
179 pkgname=ayllu-dev
180- pkgver="$VERSION"
181+ pkgver="$AYLLU_VERSION"
182 pkgrel=1
183 pkgdesc="Ayllu Forge"
184 arch=("x86_64")
185 license=("AGPL3")
186 url="https://ayllu-forge.org/ayllu/ayllu"
187- depends=()
188+ depends=(
189+ "fuse-overlayfs"
190+ )
191 makedepends=()
192 provides=("ayllu")
193- optdepends=()
194+ optdepends=(
195+ "crun"
196+ "runc"
197+ )
198 source=()
199
200 BINARIES=(
201 @@ -22,9 +30,9 @@ BINARIES=(
202
203 prepare() {
204 for i in "${!BINARIES[@]}"; do
205- cp -v "../target/release/${BINARIES[i]}" .
206+ cp -v "$CARGO_TARGET_DIR/release/${BINARIES[i]}" .
207 done
208- cp -rv ../target/dist .
209+ cp -rv ../dist .
210 find dist/man -type f -exec gzip -f '{}' \;
211 cp -v ../LICENSE .
212 cp -v ../config.example.toml .
213 diff --git a/ayllu-build/src/error.rs b/ayllu-build/src/error.rs
214index 0f70c65..9108c28 100644
215--- a/ayllu-build/src/error.rs
216+++ b/ayllu-build/src/error.rs
217 @@ -10,6 +10,8 @@ pub enum Error {
218 RepositoryDoesNotContainManifest { path: PathBuf },
219 #[error("Cannot read manifest from git repository: {0}")]
220 CannotReadManifestFromRepository(ayllu_git::Error),
221+ #[error("Cannot resolve HEAD: {0}")]
222+ CannotResolveHead(ayllu_git::Error),
223 #[error("Cannot find managed repository: {collection} {name}")]
224 CannotFindManagedRepository { collection: String, name: String },
225 #[error("Preprocessor command: {program} {args:?} failed: {io_error}")]
226 diff --git a/ayllu-build/src/main.rs b/ayllu-build/src/main.rs
227index d80b3d5..3cfcbb2 100644
228--- a/ayllu-build/src/main.rs
229+++ b/ayllu-build/src/main.rs
230 @@ -1,4 +1,4 @@
231- use std::{path::Path, time::Duration};
232+ use std::time::Duration;
233
234 use tabled::builder::Builder;
235 use tracing::Level;
236 @@ -25,38 +25,34 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
237 let cfg = config::load(cli.config.as_deref())?;
238 ayllu_logging::init(cli.level.unwrap_or(Level::INFO));
239 match cli.command {
240+ Commands::Check { source } => {
241+ let source = Source::from_arg(source.as_deref())?;
242+ let resolved = source.read(&cfg)?;
243+ let (n_workflows, n_steps) = resolved.manifest.workflows.iter().fold(
244+ (0, 0),
245+ |(n_workflows, n_steps), workflow| {
246+ (n_workflows + 1_usize, n_steps + workflow.steps.len())
247+ },
248+ );
249+ println!("Manifest contains {n_workflows} workflows and {n_steps} steps");
250+ Ok(())
251+ }
252 Commands::Evaluate {
253 source,
254 stdin,
255 name,
256 forward,
257+ debug,
258 } => {
259 let source = if stdin {
260 tracing::info!("Reading source from stdin for build {name:?}");
261 Source::from_stdin()?
262 } else {
263- match source {
264- Some(path) => {
265- let path = Path::new(&path);
266- if path.is_file() {
267- Source::Manifest {
268- path: path.to_path_buf(),
269- }
270- } else {
271- Source::Repository {
272- path: path.to_path_buf(),
273- reference: None,
274- }
275- }
276- }
277- None => Source::Repository {
278- path: std::fs::canonicalize(Path::new("."))?,
279- reference: None,
280- },
281- }
282+ Source::from_arg(source.as_deref())?
283 };
284 let runtime = RuntimeBuilder::default()
285 .config(cfg.clone())
286+ .debug(debug)
287 .oci_runtime(
288 OciRuntimeBuilder::default()
289 .config(cfg.build.clone())
290 @@ -75,6 +71,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
291 } => {
292 let runtime = RuntimeBuilder::default()
293 .config(cfg.clone())
294+ .debug(false)
295 .oci_runtime(
296 OciRuntimeBuilder::default()
297 .config(cfg.build.clone())
298 diff --git a/ayllu-build/src/manifest.rs b/ayllu-build/src/manifest.rs
299index 0254e59..80c2c2e 100644
300--- a/ayllu-build/src/manifest.rs
301+++ b/ayllu-build/src/manifest.rs
302 @@ -8,11 +8,17 @@ use crate::error::Error;
303
304 // pub type TestGraph = DiGraph<(Job, DiGraph<Step, i32>), i32>;
305
306+ fn serde_false() -> bool {
307+ false
308+ }
309+
310 #[derive(Serialize, Deserialize, Debug, Clone)]
311 pub struct Cache {
312 pub name: String,
313 pub path: PathBuf,
314+ #[serde(default = "serde_false")]
315 pub read_only: bool,
316+ #[serde(default = "serde_false")]
317 pub transient: bool,
318 }
319
320 diff --git a/ayllu-build/src/oci_runtime.rs b/ayllu-build/src/oci_runtime.rs
321index 8462e15..8210748 100644
322--- a/ayllu-build/src/oci_runtime.rs
323+++ b/ayllu-build/src/oci_runtime.rs
324 @@ -76,7 +76,7 @@ impl OciRuntime {
325
326 fn check_output(&self, others: &[&str]) -> Result<std::process::Output, Error> {
327 let args = self.args(others);
328- let output = Command::new(&self.config.oci_binary.clone())
329+ let output = Command::new(self.config.oci_binary.clone())
330 .args(args.clone())
331 .envs(std::env::vars())
332 .stdout(Stdio::piped())
333 @@ -148,7 +148,7 @@ impl OciRuntime {
334 let mut cmd = Command::new(&self.config.oci_binary);
335 cmd.args(args)
336 .envs(std::env::vars())
337- // .stdin(Stdio::inherit())
338+ .stdin(Stdio::null())
339 .stdout(Stdio::inherit())
340 .stderr(Stdio::inherit());
341
342 @@ -189,6 +189,7 @@ impl OciRuntime {
343 let mut proc = Command::new(&self.config.oci_binary)
344 .args(args.clone())
345 .envs(std::env::vars())
346+ .stdin(Stdio::null())
347 .stdout(Stdio::piped())
348 .stderr(Stdio::piped())
349 .spawn()
350 @@ -381,6 +382,7 @@ impl Template {
351 // TODO: This is an entrypoint for package manager specific tooling
352 // via composefs and similar. Design is still a WIP.
353 workflow.cache.iter().for_each(|cache| {
354+ tracing::info!("Configuring rbind mount: {}: {}", cache.name, cache.path);
355 mounts.push(
356 MountBuilder::default()
357 .source(workspace.mount_path(&cache.name))
358 diff --git a/ayllu-build/src/runtime.rs b/ayllu-build/src/runtime.rs
359index 257816c..5d9c69e 100644
360--- a/ayllu-build/src/runtime.rs
361+++ b/ayllu-build/src/runtime.rs
362 @@ -1,6 +1,6 @@
363 use std::collections::HashMap;
364 use std::fs::OpenOptions;
365- use std::io::Read;
366+ use std::io::{BufRead, Read};
367 use std::ops::Add;
368 use std::os::unix::fs::MetadataExt;
369 use std::path::Path;
370 @@ -79,6 +79,7 @@ fn compute_hash(path: &Path) -> Result<String, Error> {
371 #[builder(pattern = "owned")]
372 pub struct Runtime {
373 config: Config,
374+ debug: bool,
375 oci_runtime: OciRuntime,
376 }
377
378 @@ -161,6 +162,7 @@ impl Runtime {
379 if let Some(cache) = workflow.cache.as_ref() {
380 cache.iter().try_for_each(|cache| {
381 let _ = tx.cache_create(
382+ manifest_id,
383 workflow_id,
384 &cache.name,
385 &cache.path,
386 @@ -184,6 +186,10 @@ impl Runtime {
387 input: &step.input,
388 workflow_id,
389 })?;
390+ step.environment.iter().try_for_each(|(key, value)| {
391+ let _ = tx.variable_create(step_id, key, value)?;
392+ Ok::<_, Error>(())
393+ })?;
394 by_name.insert(step.name.clone(), graph.add_node(Unit::Step(step_id)));
395 let step = tx.step_read(step_id)?;
396 steps.push(step);
397 @@ -347,6 +353,23 @@ impl Runtime {
398 next_step_id,
399 exit_code
400 );
401+ if self.debug {
402+ let (_, workspace) =
403+ active_workflow.as_ref().expect("Workflow is active");
404+ let workflow_id = workspace.id().workflow_id;
405+ eprintln!(
406+ r#"
407+ ¡¡¡💣 The Build Has Failed and Debugging Mode is Active 💣!!!
408+
409+ This build has failed and cannot be resumed, however you may still debug this
410+ session to determine what has gone wrong.
411+
412+ Run `ayllu-build shell {manifest_id} {workflow_id}` to enter into the build environment.
413+ Once you have finished debugging press enter to continue.
414+ "#
415+ );
416+ std::io::stdin().lock().read_line(&mut String::default())?;
417+ }
418 Self::close_workflow(
419 &mut conn,
420 &mut blob,
421 diff --git a/ayllu-build/src/source.rs b/ayllu-build/src/source.rs
422index 9444d8d..fe20aee 100644
423--- a/ayllu-build/src/source.rs
424+++ b/ayllu-build/src/source.rs
425 @@ -38,6 +38,31 @@ pub enum Source {
426 }
427
428 impl Source {
429+ pub fn from_arg(arg: Option<&str>) -> Result<Self, Error> {
430+ match arg {
431+ Some(path) => {
432+ let path = Path::new(&path);
433+ if path.is_file() {
434+ Ok(Source::Manifest {
435+ path: path.to_path_buf(),
436+ })
437+ } else {
438+ let repository =
439+ ayllu_git::Wrapper::new(path).map_err(Error::CannotResolveHead)?;
440+ let head = repository.default_ref().map_err(Error::CannotResolveHead)?;
441+ Ok(Source::Repository {
442+ path: path.to_path_buf(),
443+ reference: Some(head),
444+ })
445+ }
446+ }
447+ None => Ok(Source::Repository {
448+ path: std::fs::canonicalize(Path::new("."))?,
449+ reference: None,
450+ }),
451+ }
452+ }
453+
454 pub fn from_stdin() -> Result<Self, Error> {
455 let mut line = String::default();
456 let _ = std::io::stdin().read_line(&mut line)?;
457 diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs
458index 9bdd2b9..747c3b8 100644
459--- a/ayllu-shell/src/main.rs
460+++ b/ayllu-shell/src/main.rs
461 @@ -10,7 +10,7 @@ fn execute(args: Command) -> Result<(), Box<dyn std::error::Error>> {
462 let username = args
463 .username
464 .unwrap_or(std::env::var("USER").expect("USER is not set"));
465- let identity = config
466+ let _identity = config
467 .identities
468 .iter()
469 .find(|identity| identity.username == username);
470 diff --git a/ayllu-shell/src/ui/repository.rs b/ayllu-shell/src/ui/repository.rs
471index 09674b8..da5ffa0 100644
472--- a/ayllu-shell/src/ui/repository.rs
473+++ b/ayllu-shell/src/ui/repository.rs
474 @@ -1,5 +1,3 @@
475- use std::fmt::Display;
476-
477 use ayllu_git::Sites;
478
479 use crate::ui::prompt::Prompter;
480 diff --git a/ayllu-web/src/web2/routes/man.rs b/ayllu-web/src/web2/routes/man.rs
481index 6ef2ffe..f6656a6 100644
482--- a/ayllu-web/src/web2/routes/man.rs
483+++ b/ayllu-web/src/web2/routes/man.rs
484 @@ -9,7 +9,7 @@ use crate::web2::navigation;
485 use crate::web2::template::{Base, components};
486
487 // NOTE: This is circular... so xtask gen_man needs to be run first
488- static MAN_PAGES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../target/dist/man");
489+ static MAN_PAGES: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../dist/man");
490
491 #[derive(askama::Template)]
492 #[template(path = "man.html")]
493 diff --git a/crates/cmd/src/build.rs b/crates/cmd/src/build.rs
494index a4b264f..2aca7e2 100644
495--- a/crates/cmd/src/build.rs
496+++ b/crates/cmd/src/build.rs
497 @@ -3,7 +3,12 @@ use std::path::PathBuf;
498 use clap::{Parser, Subcommand};
499 use tracing::Level;
500
501- const LONG_ABOUT_DESCRIPTION: &str = r#"
502+ /// Isomorphic Build System
503+ #[derive(Parser, Debug)]
504+ #[clap(
505+ version,
506+ name = "ayllu-build",
507+ long_about = r#"
508
509 ayllu-build is an isomorphic continuous integration server that is designed
510 to minimize the iteration cycle between local and remote builds as much as
511 @@ -15,16 +20,8 @@ but it should also be compatible with runc or yuoki.
512 This binary provides a low-level interface to Ayllu builds. Usage via githooks,
513 ayllu-shell, or quipu is the intended interface for users.
514
515- The status of builds, their outputs, and logs can be read via quipu(1).
516- "#;
517-
518- /// Isomorphic Build System
519- #[derive(Parser, Debug)]
520- #[clap(
521- version,
522- name = "ayllu-build",
523- long_about=LONG_ABOUT_DESCRIPTION,
524- )]
525+ The status of builds, their outputs, and logs can be read via quipu(1)."#
526+ )]
527 pub struct Command {
528 /// Path to your configuration file
529 #[arg(short, long, value_name = "FILE")]
530 @@ -40,14 +37,31 @@ pub struct Command {
531
532 #[derive(Subcommand, Debug, PartialEq)]
533 pub enum Commands {
534+ #[clap(long_about = r#"
535+
536+ Perform as much validation as is possible about the build configuration without
537+ actually running the build. This will check that any pre-processor returns
538+ correctly, that the manifest can be parsed by Ayllu, and perform any other
539+ initialization and validation steps."#)]
540+ /// Check the build configuration
541+ Check {
542+ /// Build Source can be PATH or collection/name
543+ #[arg(value_name = "SOURCE")]
544+ source: Option<String>,
545+ },
546 /// Evaluate a local build script
547 #[clap(long_about = r#"
548+
549 Evaluate a build configuration from the given source. If --stdin is active then
550 the desired collection/name/[branch] will be read from the input. This will
551 evaluate each step in the build process until a non-continuable error is reached
552 or all steps complete successfully. You can use the quipu(1) command to inspect
553 build artifacts, logs, and other properties.
554
555+ When the --debug flag is enabled anytime a step fails the user will be prompted
556+ to attach to the failed build in an external terminal and investigate the error.
557+ Once the user is finished they can press enter and the build will continue.
558+
559 Users have many different desires in terms of configuration file format.
560 Natively Ayllu will only accept a JSON configuration file for it's build input
561 but supports the use pre-processors to allow users to specify their build in
562 @@ -59,9 +73,7 @@ You can configure it in your Ayllu config file as such:
563 [build.pre_processor]
564 source = ".ayllu-build.jsonnet"
565 program = "jsonnet"
566- args = ["-"]
567-
568- "#)]
569+ args = ["-"] "#)]
570 Evaluate {
571 /// If the standard streams should be forwarded to the terminal
572 #[arg(short, long, action)]
573 @@ -69,6 +81,9 @@ You can configure it in your Ayllu config file as such:
574 /// Read the collection / name from stdin
575 #[arg(short, long, action)]
576 stdin: bool,
577+ /// Pause on step failures so you can attach to the environment
578+ #[arg(short, long, action)]
579+ debug: bool,
580 /// Unique build name
581 #[arg(short, long)]
582 name: Option<String>,
583 @@ -79,9 +94,9 @@ You can configure it in your Ayllu config file as such:
584 // TODO: Also needs a non-interacrtive debugging environment
585 /// Launch a shell session for debugging purposes
586 #[clap(long_about = r#"
587+
588 Attach to the container environment of an active build. NOTE that if the build
589- process ends you will not be able to enter a shell.
590- "#)]
591+ process ends you will not be able to enter a shell."#)]
592 Shell {
593 /// Manifest identifier
594 manifest_id: i32,
595 @@ -90,8 +105,8 @@ process ends you will not be able to enter a shell.
596 },
597 /// List active build processes
598 #[clap(long_about = r#"
599- List active processes from runc as reported by the container runtime.
600- "#)]
601+
602+ List active processes from runc as reported by the container runtime."#)]
603 Ps {},
604 /// Garbage collect old builds
605 #[clap(long_about = r#"
606 @@ -105,8 +120,7 @@ directory under the builds/cache directory. These directories contain OCI
607 configuration files, build outputs, and other runtime files.
608
609 This will also check for any orphan processes which typically shouldn't exist
610- but occasionally may occur. See crun(1), runc(1) or youki(1) for more details.
611- "#)]
612+ but occasionally may occur. See crun(1), runc(1) or youki(1) for more details."#)]
613 Gc {
614 /// Number of milliseconds to wait before sending SIGKILL
615 #[arg(short, long)]
616 diff --git a/crates/database/src/build.rs b/crates/database/src/build.rs
617index 324dd80..a2a5a27 100644
618--- a/crates/database/src/build.rs
619+++ b/crates/database/src/build.rs
620 @@ -706,10 +706,7 @@ pub mod outputs {
621 }
622
623 pub mod cache {
624- use std::{
625- path::Path,
626- time::{SystemTime, UNIX_EPOCH},
627- };
628+ use std::path::Path;
629
630 use super::*;
631 use crate::schema::cache::{dsl, table};
632 @@ -744,6 +741,7 @@ pub mod cache {
633 impl Ptr<'_> {
634 pub fn cache_create(
635 &mut self,
636+ manifest_id: i32,
637 workflow_id: i32,
638 name: &str,
639 path: &Path,
640 @@ -755,9 +753,7 @@ pub mod cache {
641 // time considerably while allowing you to share artifacts easily
642 // across workflows.
643 let name = if transient {
644- let now = SystemTime::now();
645- let epoch_secs = now.duration_since(UNIX_EPOCH).unwrap().as_secs();
646- format!("{name}-{epoch_secs}.transient")
647+ format!("{name}-{manifest_id}.transient")
648 } else {
649 name.to_string()
650 };
651 @@ -801,6 +797,22 @@ pub mod variables {
652 }
653
654 impl Ptr<'_> {
655+ pub fn variable_create(
656+ &mut self,
657+ step_id: i32,
658+ name: &str,
659+ value: &str,
660+ ) -> Result<i32, Error> {
661+ diesel::insert_into(table)
662+ .values((
663+ dsl::step_id.eq(step_id),
664+ dsl::name.eq(name),
665+ dsl::value.eq(value),
666+ ))
667+ .returning(dsl::id)
668+ .get_result(self.0)
669+ }
670+
671 pub fn variable_read_all(&mut self, step_id: i32) -> Result<Vec<Variable>, Error> {
672 table
673 .select(Variable::as_select())
674 diff --git a/dist/man/.gitkeep b/dist/man/.gitkeep
675new file mode 100644
676index 0000000..e69de29
677--- /dev/null
678+++ b/dist/man/.gitkeep
679 diff --git a/xtask/src/main.rs b/xtask/src/main.rs
680index 91b9535..759ee4d 100644
681--- a/xtask/src/main.rs
682+++ b/xtask/src/main.rs
683 @@ -11,8 +11,8 @@ use clap_complete::{
684 generate_to,
685 };
686
687- const DIST_COMPLETE_PATH: &str = "target/dist/completion";
688- const DIST_MAN_PATH: &str = "target/dist/man";
689+ const DIST_COMPLETE_PATH: &str = "dist/completion";
690+ const DIST_MAN_PATH: &str = "dist/man";
691
692 const AYLLU_SUMMARY: &[u8] = include_bytes!("../../ayllu.7.scd");
693 const AYLLU_CONFIG: &str = include_str!("../../ayllu.5.scd");