1use gh_workflow::*;
2
3use crate::tasks::workflows::{
4 runners,
5 steps::{self, FluentBuilder, NamedJob, named},
6 vars::{self, StepOutput, WorkflowInput},
7};
8
9pub fn autofix_pr() -> Workflow {
10 let pr_number = WorkflowInput::string("pr_number", None);
11 let run_clippy = WorkflowInput::bool("run_clippy", Some(true));
12 let run_autofix = run_autofix(&pr_number, &run_clippy);
13 let commit_changes = commit_changes(&pr_number, &run_autofix);
14 named::workflow()
15 .run_name(format!("autofix PR #{pr_number}"))
16 .on(Event::default().workflow_dispatch(
17 WorkflowDispatch::default()
18 .add_input(pr_number.name, pr_number.input())
19 .add_input(run_clippy.name, run_clippy.input()),
20 ))
21 .concurrency(
22 Concurrency::new(Expression::new(format!(
23 "${{{{ github.workflow }}}}-{pr_number}"
24 )))
25 .cancel_in_progress(true),
26 )
27 .add_job(run_autofix.name.clone(), run_autofix.job)
28 .add_job(commit_changes.name, commit_changes.job)
29}
30
31const PATCH_ARTIFACT_NAME: &str = "autofix-patch";
32const PATCH_FILE_PATH: &str = "autofix.patch";
33
34fn upload_patch_artifact() -> Step<Use> {
35 Step::new(format!("upload artifact {}", PATCH_ARTIFACT_NAME))
36 .uses(
37 "actions",
38 "upload-artifact",
39 "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5
40 )
41 .add_with(("name", PATCH_ARTIFACT_NAME))
42 .add_with(("path", PATCH_FILE_PATH))
43 .add_with(("if-no-files-found", "ignore"))
44 .add_with(("retention-days", "1"))
45}
46
47fn download_patch_artifact() -> Step<Use> {
48 named::uses(
49 "actions",
50 "download-artifact",
51 "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0
52 )
53 .add_with(("name", PATCH_ARTIFACT_NAME))
54}
55
56fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJob {
57 fn checkout_pr(pr_number: &WorkflowInput) -> Step<Run> {
58 named::bash(r#"gh pr checkout "$PR_NUMBER""#)
59 .add_env(("PR_NUMBER", pr_number.to_string()))
60 .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
61 }
62
63 fn install_cargo_machete() -> Step<Use> {
64 named::uses(
65 "clechasseur",
66 "rs-cargo",
67 "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2
68 )
69 .add_with(("command", "install"))
70 .add_with(("args", "cargo-machete@0.7.0"))
71 }
72
73 fn run_cargo_fmt() -> Step<Run> {
74 named::bash("cargo fmt --all")
75 }
76
77 fn run_cargo_fix() -> Step<Run> {
78 named::bash(
79 "cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged",
80 )
81 }
82
83 fn run_cargo_machete_fix() -> Step<Run> {
84 named::bash("cargo machete --fix")
85 }
86
87 fn run_clippy_fix() -> Step<Run> {
88 named::bash(
89 "cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged",
90 )
91 }
92
93 fn run_prettier_fix() -> Step<Run> {
94 named::bash("./script/prettier --write")
95 }
96
97 fn create_patch() -> Step<Run> {
98 named::bash(indoc::indoc! {r#"
99 if git diff --quiet; then
100 echo "No changes to commit"
101 echo "has_changes=false" >> "$GITHUB_OUTPUT"
102 else
103 git diff > autofix.patch
104 echo "has_changes=true" >> "$GITHUB_OUTPUT"
105 fi
106 "#})
107 .id("create-patch")
108 }
109
110 named::job(
111 Job::default()
112 .runs_on(runners::LINUX_DEFAULT)
113 .outputs([(
114 "has_changes".to_owned(),
115 "${{ steps.create-patch.outputs.has_changes }}".to_owned(),
116 )])
117 .add_step(steps::checkout_repo())
118 .add_step(checkout_pr(pr_number))
119 .add_step(steps::setup_cargo_config(runners::Platform::Linux))
120 .add_step(steps::cache_rust_dependencies_namespace())
121 .map(steps::install_linux_dependencies)
122 .add_step(steps::setup_pnpm())
123 .add_step(install_cargo_machete().if_condition(Expression::new(run_clippy.to_string())))
124 .add_step(run_cargo_fix().if_condition(Expression::new(run_clippy.to_string())))
125 .add_step(run_cargo_machete_fix().if_condition(Expression::new(run_clippy.to_string())))
126 .add_step(run_clippy_fix().if_condition(Expression::new(run_clippy.to_string())))
127 .add_step(run_prettier_fix())
128 .add_step(run_cargo_fmt())
129 .add_step(create_patch())
130 .add_step(upload_patch_artifact())
131 .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)),
132 )
133}
134
135fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob {
136 fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
137 named::bash(r#"gh pr checkout "$PR_NUMBER""#)
138 .add_env(("PR_NUMBER", pr_number.to_string()))
139 .add_env(("GITHUB_TOKEN", token))
140 }
141
142 fn apply_patch() -> Step<Run> {
143 named::bash("git apply autofix.patch")
144 }
145
146 fn commit_and_push(token: &StepOutput) -> Step<Run> {
147 named::bash(indoc::indoc! {r#"
148 git commit -am "Autofix"
149 git push
150 "#})
151 .add_env(("GIT_COMMITTER_NAME", "Zed Zippy"))
152 .add_env((
153 "GIT_COMMITTER_EMAIL",
154 "234243425+zed-zippy[bot]@users.noreply.github.com",
155 ))
156 .add_env(("GIT_AUTHOR_NAME", "Zed Zippy"))
157 .add_env((
158 "GIT_AUTHOR_EMAIL",
159 "234243425+zed-zippy[bot]@users.noreply.github.com",
160 ))
161 .add_env(("GITHUB_TOKEN", token))
162 }
163
164 let (authenticate, token) = steps::authenticate_as_zippy();
165
166 named::job(
167 Job::default()
168 .runs_on(runners::LINUX_SMALL)
169 .needs(vec![autofix_job.name.clone()])
170 .cond(Expression::new(format!(
171 "needs.{}.outputs.has_changes == 'true'",
172 autofix_job.name
173 )))
174 .add_step(authenticate)
175 .add_step(steps::checkout_repo().with_token(&token))
176 .add_step(checkout_pr(pr_number, &token))
177 .add_step(download_patch_artifact())
178 .add_step(apply_patch())
179 .add_step(commit_and_push(&token)),
180 )
181}