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 .add_job(run_autofix.name.clone(), run_autofix.job)
22 .add_job(commit_changes.name, commit_changes.job)
23}
24
25const PATCH_ARTIFACT_NAME: &str = "autofix-patch";
26const PATCH_FILE_PATH: &str = "autofix.patch";
27
28fn upload_patch_artifact() -> Step<Use> {
29 Step::new(format!("upload artifact {}", PATCH_ARTIFACT_NAME))
30 .uses(
31 "actions",
32 "upload-artifact",
33 "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5
34 )
35 .add_with(("name", PATCH_ARTIFACT_NAME))
36 .add_with(("path", PATCH_FILE_PATH))
37 .add_with(("if-no-files-found", "ignore"))
38 .add_with(("retention-days", "1"))
39}
40
41fn download_patch_artifact() -> Step<Use> {
42 named::uses(
43 "actions",
44 "download-artifact",
45 "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0
46 )
47 .add_with(("name", PATCH_ARTIFACT_NAME))
48}
49
50fn run_autofix(pr_number: &WorkflowInput, run_clippy: &WorkflowInput) -> NamedJob {
51 fn checkout_pr(pr_number: &WorkflowInput) -> Step<Run> {
52 named::bash(&format!("gh pr checkout {pr_number}"))
53 .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN))
54 }
55
56 fn run_cargo_fmt() -> Step<Run> {
57 named::bash("cargo fmt --all")
58 }
59
60 fn run_clippy_fix() -> Step<Run> {
61 named::bash(
62 "cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged",
63 )
64 }
65
66 fn run_prettier_fix() -> Step<Run> {
67 named::bash("./script/prettier --write")
68 }
69
70 fn create_patch() -> Step<Run> {
71 named::bash(indoc::indoc! {r#"
72 if git diff --quiet; then
73 echo "No changes to commit"
74 echo "has_changes=false" >> "$GITHUB_OUTPUT"
75 else
76 git diff > autofix.patch
77 echo "has_changes=true" >> "$GITHUB_OUTPUT"
78 fi
79 "#})
80 .id("create-patch")
81 }
82
83 named::job(
84 Job::default()
85 .runs_on(runners::LINUX_DEFAULT)
86 .outputs([(
87 "has_changes".to_owned(),
88 "${{ steps.create-patch.outputs.has_changes }}".to_owned(),
89 )])
90 .add_step(steps::checkout_repo())
91 .add_step(checkout_pr(pr_number))
92 .add_step(steps::setup_cargo_config(runners::Platform::Linux))
93 .add_step(steps::cache_rust_dependencies_namespace())
94 .map(steps::install_linux_dependencies)
95 .add_step(steps::setup_pnpm())
96 .add_step(run_prettier_fix())
97 .add_step(run_cargo_fmt())
98 .add_step(run_clippy_fix().if_condition(Expression::new(run_clippy.to_string())))
99 .add_step(create_patch())
100 .add_step(upload_patch_artifact())
101 .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)),
102 )
103}
104
105fn commit_changes(pr_number: &WorkflowInput, autofix_job: &NamedJob) -> NamedJob {
106 fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
107 let step = named::uses(
108 "actions",
109 "create-github-app-token",
110 "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
111 )
112 .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
113 .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
114 .id("get-app-token");
115 let output = StepOutput::new(&step, "token");
116 (step, output)
117 }
118
119 fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
120 named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
121 }
122
123 fn apply_patch() -> Step<Run> {
124 named::bash("git apply autofix.patch")
125 }
126
127 fn commit_and_push(token: &StepOutput) -> Step<Run> {
128 named::bash(indoc::indoc! {r#"
129 git commit -am "Autofix"
130 git push
131 "#})
132 .add_env(("GIT_COMMITTER_NAME", "Zed Zippy"))
133 .add_env((
134 "GIT_COMMITTER_EMAIL",
135 "234243425+zed-zippy[bot]@users.noreply.github.com",
136 ))
137 .add_env(("GIT_AUTHOR_NAME", "Zed Zippy"))
138 .add_env((
139 "GIT_AUTHOR_EMAIL",
140 "234243425+zed-zippy[bot]@users.noreply.github.com",
141 ))
142 .add_env(("GITHUB_TOKEN", token))
143 }
144
145 let (authenticate, token) = authenticate_as_zippy();
146
147 named::job(
148 Job::default()
149 .runs_on(runners::LINUX_SMALL)
150 .needs(vec![autofix_job.name.clone()])
151 .cond(Expression::new(format!(
152 "needs.{}.outputs.has_changes == 'true'",
153 autofix_job.name
154 )))
155 .add_step(authenticate)
156 .add_step(steps::checkout_repo_with_token(&token))
157 .add_step(checkout_pr(pr_number, &token))
158 .add_step(download_patch_artifact())
159 .add_step(apply_patch())
160 .add_step(commit_and_push(&token)),
161 )
162}