gh_workflow_tailcall/
standard.rs1use ctx::Context;
9use derive_setters::Setters;
10use generate::Generate;
11use gh_workflow::error::Result;
12use gh_workflow::{Workflow as GHWorkflow, *};
13use heck::ToTitleCase;
14use release_plz::{Command, Release};
15use toolchain::Toolchain;
16
17#[derive(Debug, Clone, Default)]
19pub enum TestRunner {
20 Cargo,
22
23 #[default]
25 Nextest,
26}
27
28#[derive(Debug, Clone, Setters)]
29pub struct StandardWorkflow {
30 pub auto_release: bool,
34
35 pub name: String,
37
38 pub benchmarks: bool,
40
41 pub auto_fix: bool,
43
44 pub setup: Vec<Step<Run>>,
46
47 pub test_runner: TestRunner,
49}
50
51impl Default for StandardWorkflow {
52 fn default() -> Self {
53 Self {
54 auto_release: false,
55 name: "ci".into(),
56 benchmarks: false,
57 auto_fix: false,
58 setup: Vec::new(),
59 test_runner: TestRunner::default(),
60 }
61 }
62}
63
64impl StandardWorkflow {
65 fn init_job(&self, name: impl ToString) -> Job {
72 let mut job = Job::new(name).permissions(Permissions::default().contents(Level::Read));
73
74 for step in self.setup.iter().rev() {
76 job = job.add_step(step.clone());
77 }
78
79 job.add_step(Step::checkout())
80 }
81
82 pub fn add_setup<S: Into<Step<Run>>>(mut self, step: S) -> Self {
92 self.setup.push(step.into());
93 self
94 }
95}
96
97impl StandardWorkflow {
98 pub fn generate(self) -> Result<()> {
100 self.to_ci_workflow().generate()?;
101 Generate::new(self.to_autofix_workflow())
102 .name("autofix.yml")
103 .generate()?;
104 Ok(())
105 }
106
107 fn to_autofix_workflow(&self) -> GHWorkflow {
109 GHWorkflow::new("autofix.ci")
111 .add_env(self.workflow_flags())
112 .on(self.workflow_event())
113 .add_job("lint", self.lint_job(true))
114 }
115
116 pub fn to_ci_workflow(&self) -> GHWorkflow {
118 GHWorkflow::new(self.name.clone())
119 .add_env(self.workflow_flags())
120 .on(self.workflow_event())
121 .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 )
133 }
134
135 fn release_job(&self, cmd: Command) -> Job {
136 self.init_job(cmd.to_string().to_title_case())
137 .concurrency(
138 Concurrency::new(Expression::new("release-${{github.ref}}"))
139 .cancel_in_progress(false),
140 )
141 .cond(self.workflow_cond())
142 .add_needs(self.test_job())
143 .add_needs(self.lint_job(false))
144 .add_env(Env::github())
145 .add_env(Env::new(
146 "CARGO_REGISTRY_TOKEN",
147 "${{ secrets.CARGO_REGISTRY_TOKEN }}",
148 ))
149 .permissions(self.write_permissions())
150 .add_step(Release::default().command(cmd))
151 }
152
153 fn lint_job(&self, auto_fix: bool) -> Job {
154 let job = self.init_job(if auto_fix { "Lint Fix" } else { "Lint" });
155
156 let job = if auto_fix {
157 job.concurrency(
158 Concurrency::new(Expression::new("autofix-${{github.ref}}"))
159 .cancel_in_progress(false),
160 )
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 )
200 }
201
202 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),
208 Cargo::new("install")
209 .args("cargo-nextest --locked")
210 .name("Install nextest"),
211 )
212 .add_step(match self.test_runner {
213 TestRunner::Cargo => Cargo::new("test")
214 .args("--all-features --workspace")
215 .name("Cargo Test"),
216 TestRunner::Nextest => Cargo::new("nextest")
217 .args("run --all-features --workspace")
218 .name("Cargo Nextest"),
219 })
220 .add_step_when(
221 self.benchmarks,
222 Cargo::new("bench").args("--workspace").name("Cargo Bench"),
223 )
224 }
225
226 fn write_permissions(&self) -> Permissions {
227 Permissions::default()
228 .pull_requests(Level::Write)
229 .packages(Level::Write)
230 .contents(Level::Write)
231 }
232
233 fn workflow_cond(&self) -> Context<bool> {
234 let is_main = Context::github().ref_().eq("refs/heads/main".into());
235 let is_push = Context::github().event_name().eq("push".into());
236
237 is_main.and(is_push)
238 }
239
240 fn workflow_event(&self) -> Event {
241 Event::default()
242 .push(Push::default().add_branch("main"))
243 .pull_request(
244 PullRequest::default()
245 .add_type(PullRequestType::Opened)
246 .add_type(PullRequestType::Synchronize)
247 .add_type(PullRequestType::Reopened)
248 .add_branch("main"),
249 )
250 }
251
252 fn workflow_flags(&self) -> RustFlags {
253 RustFlags::deny("warnings")
254 }
255}