Skip to content

Commit 13f2c10

Browse files
refactor: improve API ergonomics with builder pattern and method chaining (#165)
* refactor: add `borrow_self` to setters for various structs * refactor: update method signatures to return mutable references for better chaining * refactor: simplify job dependency management and streamline needs handling * refactor: remove redundant `add_steps` method from `Job` struct * revert borrow * refactor: change methods to return self for better chaining in Cargo and workflow structs * refactor: introduce Job and Step structures for GitHub workflow management * refactor: add Concurrency and Permissions structures for workflow management * feat: add Container configuration types for GitHub workflow jobs * feat: add various structures for GitHub workflow management including artifacts, environment, defaults, and strategies * refactor: update Step creation methods to use new constructor for better clarity and consistency * refactor: enhance Toolchain structure and methods for improved clarity and functionality * fix: update autofix action version to v1 for improved stability * [autofix.ci] apply automated fixes * fix: update autofix action version to v1 for improved stability --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent f1fc651 commit 13f2c10

File tree

21 files changed

+944
-992
lines changed

21 files changed

+944
-992
lines changed

.github/workflows/autofix.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ jobs:
5454
run: cargo +nightly fmt --all
5555
- name: Cargo Clippy
5656
run: cargo +nightly clippy --fix --allow-dirty --all-features --workspace -- -D warnings
57-
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef
57+
- name: auto-fix
58+
uses: autofix-ci/action@v1
5859
concurrency:
5960
group: autofix-${{github.ref}}
6061
cancel-in-progress: false

crates/gh-workflow-tailcall/src/standard.rs

Lines changed: 73 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub enum TestRunner {
2626
}
2727

2828
#[derive(Debug, Clone, Setters)]
29+
#[setters(strip_option, into)]
2930
pub struct StandardWorkflow {
3031
/// When enabled, a release job is added to the workflow.
3132
/// *IMPORTANT:* Ensure `secrets.CARGO_REGISTRY_TOKEN` is set for your
@@ -115,21 +116,19 @@ impl StandardWorkflow {
115116

116117
/// Converts the workflow into a Github workflow.
117118
pub fn to_ci_workflow(&self) -> GHWorkflow {
118-
GHWorkflow::new(self.name.clone())
119+
let mut workflow = GHWorkflow::new(self.name.clone())
119120
.add_env(self.workflow_flags())
120121
.on(self.workflow_event())
121122
.add_job("build", self.test_job())
122-
.add_job("lint", self.lint_job(false))
123-
.add_job_when(
124-
self.auto_release,
125-
"release",
126-
self.release_job(Command::Release),
127-
)
128-
.add_job_when(
129-
self.auto_release,
130-
"release-pr",
131-
self.release_job(Command::ReleasePR),
132-
)
123+
.add_job("lint", self.lint_job(false));
124+
125+
if self.auto_release {
126+
workflow = workflow
127+
.add_job("release", self.release_job(Command::Release))
128+
.add_job("release-pr", self.release_job(Command::ReleasePR));
129+
}
130+
131+
workflow
133132
}
134133

135134
fn release_job(&self, cmd: Command) -> Job {
@@ -139,8 +138,8 @@ impl StandardWorkflow {
139138
.cancel_in_progress(false),
140139
)
141140
.cond(self.workflow_cond())
142-
.add_needs(self.test_job())
143-
.add_needs(self.lint_job(false))
141+
.add_needs("build")
142+
.add_needs("lint")
144143
.add_env(Env::github())
145144
.add_env(Env::new(
146145
"CARGO_REGISTRY_TOKEN",
@@ -151,67 +150,71 @@ impl StandardWorkflow {
151150
}
152151

153152
fn lint_job(&self, auto_fix: bool) -> Job {
154-
let job = self.init_job(if auto_fix { "Lint Fix" } else { "Lint" });
153+
let mut job = self.init_job(if auto_fix { "Lint Fix" } else { "Lint" });
155154

156-
let job = if auto_fix {
157-
job.concurrency(
155+
if auto_fix {
156+
job = job.concurrency(
158157
Concurrency::new(Expression::new("autofix-${{github.ref}}"))
159158
.cancel_in_progress(false),
159+
);
160+
}
161+
162+
let mut fmt_step = Cargo::new("fmt")
163+
.name("Cargo Fmt")
164+
.nightly()
165+
.add_args("--all");
166+
167+
if !auto_fix {
168+
fmt_step = fmt_step.add_args("--check");
169+
}
170+
171+
let mut clippy_step = Cargo::new("clippy").name("Cargo Clippy").nightly();
172+
173+
if auto_fix {
174+
clippy_step = clippy_step.add_args("--fix").add_args("--allow-dirty");
175+
}
176+
177+
clippy_step = clippy_step.add_args("--all-features --workspace -- -D warnings");
178+
179+
job = job
180+
.add_step(
181+
Toolchain::default()
182+
.add_nightly()
183+
.add_clippy()
184+
.add_fmt()
185+
.cache(true)
186+
.cache_directories(vec![
187+
"~/.cargo/registry".into(),
188+
"~/.cargo/git".into(),
189+
"target".into(),
190+
]),
160191
)
161-
} else {
162-
job
163-
};
164-
165-
job.add_step(
166-
Toolchain::default()
167-
.add_nightly()
168-
.add_clippy()
169-
.add_fmt()
170-
.cache(true)
171-
.cache_directories(vec![
172-
"~/.cargo/registry".into(),
173-
"~/.cargo/git".into(),
174-
"target".into(),
175-
]),
176-
)
177-
.add_step(
178-
Cargo::new("fmt")
179-
.name("Cargo Fmt")
180-
.nightly()
181-
.add_args("--all")
182-
.add_args_when(!auto_fix, "--check"),
183-
)
184-
.add_step(
185-
Cargo::new("clippy")
186-
.name("Cargo Clippy")
187-
.nightly()
188-
.add_args_when(auto_fix, "--fix")
189-
.add_args_when(auto_fix, "--allow-dirty")
190-
.add_args("--all-features --workspace -- -D warnings"),
191-
)
192-
.add_step_when(
193-
auto_fix,
194-
Step::uses(
195-
"autofix-ci",
196-
"action",
197-
"551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef",
198-
),
199-
)
192+
.add_step(fmt_step)
193+
.add_step(clippy_step);
194+
195+
if auto_fix {
196+
job = job.add_step(Step::new("auto-fix").uses("autofix-ci", "action", "v1"));
197+
}
198+
job
200199
}
201200

202201
/// Creates the "Build and Test" job for the workflow.
203202
fn test_job(&self) -> Job {
204-
self.init_job("Build and Test")
205-
.add_step(Toolchain::default().add_stable())
206-
.add_step_when(
207-
matches!(self.test_runner, TestRunner::Nextest),
203+
let mut job = self
204+
.init_job("Build and Test")
205+
.add_step(Toolchain::default().add_stable());
206+
207+
if matches!(self.test_runner, TestRunner::Nextest) {
208+
job = job.add_step(
208209
Cargo::new("install")
209210
.args("cargo-nextest --locked")
210211
.name("Install nextest"),
211-
)
212+
);
213+
}
214+
job = job
212215
.add_step(
213-
Step::uses("Swatinem", "rust-cache", "v2")
214-
.name("Cache Rust dependencies")
216+
Step::new("Cache Rust dependencies")
217+
.uses("Swatinem", "rust-cache", "v2")
215218
.add_with(("cache-all-crates", "true")),
216219
)
217220
.add_step(match self.test_runner {
@@ -221,11 +224,13 @@ impl StandardWorkflow {
221224
TestRunner::Nextest => Cargo::new("nextest")
222225
.args("run --all-features --workspace")
223226
.name("Cargo Nextest"),
224-
})
225-
.add_step_when(
226-
self.benchmarks,
227-
Cargo::new("bench").args("--workspace").name("Cargo Bench"),
228-
)
227+
});
228+
229+
if self.benchmarks {
230+
job = job.add_step(Cargo::new("bench").args("--workspace").name("Cargo Bench"));
231+
}
232+
233+
job
229234
}
230235

231236
fn write_permissions(&self) -> Permissions {

crates/gh-workflow-tailcall/tests/ci.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ fn generate() {
55
StandardWorkflow::default()
66
.auto_release(true)
77
.auto_fix(true)
8+
.to_owned()
89
.generate()
910
.unwrap();
1011
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//!
2+
//! Artifact types for GitHub workflow job outputs and inputs.
3+
4+
use derive_setters::Setters;
5+
use serde::{Deserialize, Serialize};
6+
7+
/// Represents artifacts produced by jobs.
8+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
9+
#[serde(rename_all = "kebab-case")]
10+
#[setters(strip_option, into)]
11+
pub struct Artifacts {
12+
/// Artifacts to upload after the job.
13+
#[serde(skip_serializing_if = "Option::is_none")]
14+
pub upload: Option<Vec<Artifact>>,
15+
16+
/// Artifacts to download before the job.
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub download: Option<Vec<Artifact>>,
19+
}
20+
21+
/// Represents an artifact produced by a job.
22+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
23+
#[serde(rename_all = "kebab-case")]
24+
#[setters(strip_option, into)]
25+
pub struct Artifact {
26+
/// The name of the artifact.
27+
pub name: String,
28+
29+
/// The path to the artifact.
30+
pub path: String,
31+
32+
/// The number of days to retain the artifact.
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub retention_days: Option<u32>,
35+
}

crates/gh-workflow/src/cargo.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,6 @@ impl Cargo {
5858
);
5959
self
6060
}
61-
62-
/// Adds the arguments to the cargo command when a condition is met.
63-
pub fn add_args_when<T: ToString>(self, when: bool, args: T) -> Self {
64-
if when {
65-
self.add_args(args)
66-
} else {
67-
self
68-
}
69-
}
7061
}
7162

7263
impl From<Cargo> for Step<Run> {
@@ -77,7 +68,7 @@ impl From<Cargo> for Step<Run> {
7768
command.push(format!("+{toolchain}"));
7869
}
7970

80-
command.push(value.command);
71+
command.push(value.command.clone());
8172

8273
// Extend the command with non-empty arguments
8374
command.extend(
@@ -88,7 +79,7 @@ impl From<Cargo> for Step<Run> {
8879
.filter(|arg| !arg.is_empty()),
8980
);
9081

91-
let mut step = Step::run(command.join(" "));
82+
let mut step = Step::new(format!("Cargo {}", value.command)).run(command.join(" "));
9283

9384
if let Some(id) = value.id {
9485
step = step.id(id);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use derive_setters::Setters;
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::expression::Expression;
5+
6+
/// Represents concurrency settings for workflows.
7+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
8+
#[serde(rename_all = "kebab-case")]
9+
#[setters(strip_option, into)]
10+
pub struct Concurrency {
11+
/// The group name for concurrency.
12+
pub group: String,
13+
14+
/// Whether to cancel in-progress jobs.
15+
#[serde(skip_serializing_if = "Option::is_none")]
16+
pub cancel_in_progress: Option<bool>,
17+
18+
/// The limit on concurrent jobs.
19+
#[serde(skip_serializing_if = "Option::is_none")]
20+
pub limit: Option<u32>,
21+
}
22+
23+
impl Concurrency {
24+
pub fn new(group: impl Into<Expression>) -> Self {
25+
let expr: Expression = group.into();
26+
Self { group: expr.0, ..Default::default() }
27+
}
28+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//! Container configuration types for GitHub workflow jobs.
2+
3+
use derive_setters::Setters;
4+
use serde::{Deserialize, Serialize};
5+
6+
use crate::env::Env;
7+
8+
/// Represents a container configuration for jobs.
9+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
10+
#[serde(rename_all = "kebab-case")]
11+
#[setters(strip_option, into)]
12+
pub struct Container {
13+
/// The image to use for the container.
14+
pub image: String,
15+
16+
/// Credentials for accessing the container.
17+
#[serde(skip_serializing_if = "Option::is_none")]
18+
pub credentials: Option<Credentials>,
19+
20+
/// Environment variables for the container.
21+
#[serde(skip_serializing_if = "Option::is_none")]
22+
pub env: Option<Env>,
23+
24+
/// Ports to expose from the container.
25+
#[serde(skip_serializing_if = "Option::is_none")]
26+
pub ports: Option<Vec<Port>>,
27+
28+
/// Volumes to mount in the container.
29+
#[serde(skip_serializing_if = "Option::is_none")]
30+
pub volumes: Option<Vec<Volume>>,
31+
32+
/// Additional options for the container.
33+
#[serde(skip_serializing_if = "Option::is_none")]
34+
pub options: Option<String>,
35+
36+
/// Hostname for the container.
37+
#[serde(skip_serializing_if = "Option::is_none")]
38+
pub hostname: Option<String>,
39+
}
40+
41+
/// Represents credentials for accessing a container.
42+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
43+
#[serde(rename_all = "kebab-case")]
44+
#[setters(strip_option, into)]
45+
pub struct Credentials {
46+
/// The username for authentication.
47+
pub username: String,
48+
49+
/// The password for authentication.
50+
pub password: String,
51+
}
52+
53+
/// Represents a network port.
54+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
55+
#[serde(rename_all = "kebab-case")]
56+
pub enum Port {
57+
/// A port specified by its number.
58+
Number(u16),
59+
60+
/// A port specified by its name.
61+
Name(String),
62+
}
63+
64+
/// Represents a volume configuration for containers.
65+
#[derive(Debug, Setters, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
66+
#[serde(rename_all = "kebab-case")]
67+
#[setters(strip_option, into)]
68+
pub struct Volume {
69+
/// The source path of the volume.
70+
pub source: String,
71+
72+
/// The destination path of the volume.
73+
pub destination: String,
74+
}
75+
76+
impl Volume {
77+
/// Creates a new `Volume` from a string representation.
78+
pub fn new(volume_str: &str) -> Option<Self> {
79+
let parts: Vec<&str> = volume_str.split(':').collect();
80+
if parts.len() == 2 {
81+
Some(Volume {
82+
source: parts[0].to_string(),
83+
destination: parts[1].to_string(),
84+
})
85+
} else {
86+
None
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)