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        .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}