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)]
29#[setters(strip_option, into)]
30pub struct StandardWorkflow {
31 pub auto_release: bool,
35
36 pub name: String,
38
39 pub benchmarks: bool,
41
42 pub auto_fix: bool,
44
45 pub setup: Vec<Step<Run>>,
47
48 pub test_runner: TestRunner,
50}
51
52impl Default for StandardWorkflow {
53 fn default() -> Self {
54 Self {
55 auto_release: false,
56 name: "ci".into(),
57 benchmarks: false,
58 auto_fix: false,
59 setup: Vec::new(),
60 test_runner: TestRunner::default(),
61 }
62 }
63}
64
65impl StandardWorkflow {
66 fn init_job(&self, name: impl ToString) -> Job {
73 let mut job = Job::new(name).permissions(Permissions::default().contents(Level::Read));
74
75 for step in self.setup.iter().rev() {
77 job = job.add_step(step.clone());
78 }
79
80 job.add_step(Step::checkout())
81 }
82
83 pub fn add_setup<S: Into<Step<Run>>>(mut self, step: S) -> Self {
93 self.setup.push(step.into());
94 self
95 }
96}
97
98impl StandardWorkflow {
99 pub fn generate(self) -> Result<()> {
101 self.to_ci_workflow().generate()?;
102 Generate::new(self.to_autofix_workflow())
103 .name("autofix.yml")
104 .generate()?;
105 Ok(())
106 }
107
108 fn to_autofix_workflow(&self) -> GHWorkflow {
110 GHWorkflow::new("autofix.ci")
112 .add_env(self.workflow_flags())
113 .on(self.workflow_event())
114 .add_job("lint", self.lint_job(true))
115 }
116
117 pub fn to_ci_workflow(&self) -> GHWorkflow {
119 let mut workflow = GHWorkflow::new(self.name.clone())
120 .add_env(self.workflow_flags())
121 .on(self.workflow_event())
122 .add_job("build", self.test_job())
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
132 }
133
134 fn release_job(&self, cmd: Command) -> Job {
135 self.init_job(cmd.to_string().to_title_case())
136 .concurrency(
137 Concurrency::new(Expression::new("release-${{github.ref}}"))
138 .cancel_in_progress(false),
139 )
140 .cond(self.workflow_cond())
141 .add_needs("build")
142 .add_needs("lint")
143 .add_env(Env::github())
144 .add_env(Env::new(
145 "CARGO_REGISTRY_TOKEN",
146 "${{ secrets.CARGO_REGISTRY_TOKEN }}",
147 ))
148 .permissions(self.write_permissions())
149 .add_step(Release::default().command(cmd))
150 }
151
152 fn lint_job(&self, auto_fix: bool) -> Job {
153 let mut job = self.init_job(if auto_fix { "Lint Fix" } else { "Lint" });
154
155 if auto_fix {
156 job = job.concurrency(
157 Concurrency::new(Expression::new("autofix-${{github.ref}}"))
158 .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 ]),
191 )
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
199 }
200
201 fn test_job(&self) -> Job {
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(
209 Step::new("Install nextest")
210 .uses("taiki-e", "install-action", "v2")
211 .add_with(("tool", "nextest")),
212 );
213 }
214 job = job
215 .add_step(
216 Step::new("Cache Rust dependencies")
217 .uses("Swatinem", "rust-cache", "v2")
218 .add_with(("cache-all-crates", "true")),
219 )
220 .add_step(match self.test_runner {
221 TestRunner::Cargo => Cargo::new("test")
222 .args("--all-features --workspace")
223 .name("Cargo Test"),
224 TestRunner::Nextest => Cargo::new("nextest")
225 .args("run --all-features --workspace")
226 .name("Cargo Nextest"),
227 });
228
229 if self.benchmarks {
230 job = job.add_step(Cargo::new("bench").args("--workspace").name("Cargo Bench"));
231 }
232
233 job
234 }
235
236 fn write_permissions(&self) -> Permissions {
237 Permissions::default()
238 .pull_requests(Level::Write)
239 .packages(Level::Write)
240 .contents(Level::Write)
241 }
242
243 fn workflow_cond(&self) -> Context<bool> {
244 let is_main = Context::github().ref_().eq("refs/heads/main".into());
245 let is_push = Context::github().event_name().eq("push".into());
246
247 is_main.and(is_push)
248 }
249
250 fn workflow_event(&self) -> Event {
251 Event::default()
252 .push(Push::default().add_branch("main").add_tag("v*"))
253 .pull_request(
254 PullRequest::default()
255 .add_type(PullRequestType::Opened)
256 .add_type(PullRequestType::Synchronize)
257 .add_type(PullRequestType::Reopened)
258 .add_branch("main"),
259 )
260 }
261
262 fn workflow_flags(&self) -> RustFlags {
263 RustFlags::deny("warnings")
264 }
265}