autofix_pr.rs

  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}