Add an autofix workflow (#44922)

Conrad Irwin created

One of the major annoyances with writing code with claude is that its
poorly indented; instead of requiring manual intervention, let's just
fix that in CI.

Similar to https://autofix.ci, but as we already have a github app,
we can do it without relying on a 3rd party.

This PR doesn't trigger the workflow (we need a separate change in Zippy
to do
that) but will let me test it manually.

Release Notes:

- N/A

Change summary

.github/workflows/autofix_pr.yml                | 53 +++++++++++++
tooling/xtask/src/tasks/workflows.rs            |  2 
tooling/xtask/src/tasks/workflows/autofix_pr.rs | 77 +++++++++++++++++++
tooling/xtask/src/tasks/workflows/steps.rs      | 12 ++
4 files changed, 143 insertions(+), 1 deletion(-)

Detailed changes

.github/workflows/autofix_pr.yml 🔗

@@ -0,0 +1,53 @@
+# Generated from xtask::workflows::autofix_pr
+# Rebuild with `cargo xtask workflows`.
+name: autofix_pr
+run-name: 'autofix PR #${{ inputs.pr_number }}'
+on:
+  workflow_dispatch:
+    inputs:
+      pr_number:
+        description: pr_number
+        required: true
+        type: string
+jobs:
+  run_autofix:
+    runs-on: namespace-profile-2x4-ubuntu-2404
+    steps:
+    - id: get-app-token
+      name: autofix_pr::run_autofix::authenticate_as_zippy
+      uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    - name: steps::checkout_repo_with_token
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+        token: ${{ steps.get-app-token.outputs.token }}
+    - name: autofix_pr::run_autofix::checkout_pr
+      run: gh pr checkout ${{ inputs.pr_number }}
+      shell: bash -euxo pipefail {0}
+      env:
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+    - name: autofix_pr::run_autofix::run_cargo_fmt
+      run: cargo fmt --all
+      shell: bash -euxo pipefail {0}
+    - name: autofix_pr::run_autofix::run_clippy_fix
+      run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
+      shell: bash -euxo pipefail {0}
+    - name: autofix_pr::run_autofix::commit_and_push
+      run: |
+        if git diff --quiet; then
+            echo "No changes to commit"
+        else
+            git add -A
+            git commit -m "Apply cargo fmt and clippy --fix"
+            git push
+        fi
+      shell: bash -euxo pipefail {0}
+      env:
+        GIT_COMMITTER_NAME: Zed Zippy
+        GIT_COMMITTER_EMAIL: hi@zed.dev
+        GIT_AUTHOR_NAME: Zed Zippy
+        GIT_AUTHOR_EMAIL: hi@zed.dev
+        GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}

tooling/xtask/src/tasks/workflows.rs 🔗

@@ -5,6 +5,7 @@ use std::fs;
 use std::path::{Path, PathBuf};
 
 mod after_release;
+mod autofix_pr;
 mod cherry_pick;
 mod compare_perf;
 mod danger;
@@ -111,6 +112,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
         WorkflowFile::zed(run_tests::run_tests),
         WorkflowFile::zed(release::release),
         WorkflowFile::zed(cherry_pick::cherry_pick),
+        WorkflowFile::zed(autofix_pr::autofix_pr),
         WorkflowFile::zed(compare_perf::compare_perf),
         WorkflowFile::zed(run_agent_evals::run_unit_evals),
         WorkflowFile::zed(run_agent_evals::run_cron_unit_evals),

tooling/xtask/src/tasks/workflows/autofix_pr.rs 🔗

@@ -0,0 +1,77 @@
+use gh_workflow::*;
+
+use crate::tasks::workflows::{
+    runners,
+    steps::{self, NamedJob, named},
+    vars::{self, StepOutput, WorkflowInput},
+};
+
+pub fn autofix_pr() -> Workflow {
+    let pr_number = WorkflowInput::string("pr_number", None);
+    let autofix = run_autofix(&pr_number);
+    named::workflow()
+        .run_name(format!("autofix PR #{pr_number}"))
+        .on(Event::default().workflow_dispatch(
+            WorkflowDispatch::default().add_input(pr_number.name, pr_number.input()),
+        ))
+        .add_job(autofix.name, autofix.job)
+}
+
+fn run_autofix(pr_number: &WorkflowInput) -> NamedJob {
+    fn authenticate_as_zippy() -> (Step<Use>, StepOutput) {
+        let step = named::uses(
+            "actions",
+            "create-github-app-token",
+            "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1",
+        )
+        .add_with(("app-id", vars::ZED_ZIPPY_APP_ID))
+        .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY))
+        .id("get-app-token");
+        let output = StepOutput::new(&step, "token");
+        (step, output)
+    }
+
+    fn checkout_pr(pr_number: &WorkflowInput, token: &StepOutput) -> Step<Run> {
+        named::bash(&format!("gh pr checkout {pr_number}")).add_env(("GITHUB_TOKEN", token))
+    }
+
+    fn run_cargo_fmt() -> Step<Run> {
+        named::bash("cargo fmt --all")
+    }
+
+    fn run_clippy_fix() -> Step<Run> {
+        named::bash(
+            "cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged",
+        )
+    }
+
+    fn commit_and_push(token: &StepOutput) -> Step<Run> {
+        named::bash(indoc::indoc! {r#"
+            if git diff --quiet; then
+                echo "No changes to commit"
+            else
+                git add -A
+                git commit -m "Apply cargo fmt and clippy --fix"
+                git push
+            fi
+        "#})
+        .add_env(("GIT_COMMITTER_NAME", "Zed Zippy"))
+        .add_env(("GIT_COMMITTER_EMAIL", "hi@zed.dev"))
+        .add_env(("GIT_AUTHOR_NAME", "Zed Zippy"))
+        .add_env(("GIT_AUTHOR_EMAIL", "hi@zed.dev"))
+        .add_env(("GITHUB_TOKEN", token))
+    }
+
+    let (authenticate, token) = authenticate_as_zippy();
+
+    named::job(
+        Job::default()
+            .runs_on(runners::LINUX_SMALL)
+            .add_step(authenticate)
+            .add_step(steps::checkout_repo_with_token(&token))
+            .add_step(checkout_pr(pr_number, &token))
+            .add_step(run_cargo_fmt())
+            .add_step(run_clippy_fix())
+            .add_step(commit_and_push(&token)),
+    )
+}

tooling/xtask/src/tasks/workflows/steps.rs 🔗

@@ -1,6 +1,6 @@
 use gh_workflow::*;
 
-use crate::tasks::workflows::{runners::Platform, vars};
+use crate::tasks::workflows::{runners::Platform, vars, vars::StepOutput};
 
 pub const BASH_SHELL: &str = "bash -euxo pipefail {0}";
 // https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell
@@ -17,6 +17,16 @@ pub fn checkout_repo() -> Step<Use> {
     .add_with(("clean", false))
 }
 
+pub fn checkout_repo_with_token(token: &StepOutput) -> Step<Use> {
+    named::uses(
+        "actions",
+        "checkout",
+        "11bd71901bbe5b1630ceea73d27597364c9af683", // v4
+    )
+    .add_with(("clean", false))
+    .add_with(("token", token.to_string()))
+}
+
 pub fn setup_pnpm() -> Step<Use> {
     named::uses(
         "pnpm",