ci: Generate `publish_extension_cli` with `gh_workflow` (#47890)

Finn Evers created

This moves the extension CLI job into xtask and also extends this a bit
- whenever we now run the job, it will open PRs against this repo and
`zed-industries/extensions` to also update the SHAs there. These PRs
will be assigned to the actor that initiated the bump so they can edit
the PR as needed.

Release Notes:

- N/A

Change summary

.github/workflows/publish_extension_cli.yml                | 154 ++++-
tooling/xtask/src/tasks/workflows.rs                       |   2 
tooling/xtask/src/tasks/workflows/publish_extension_cli.rs | 201 ++++++++
3 files changed, 323 insertions(+), 34 deletions(-)

Detailed changes

.github/workflows/publish_extension_cli.yml 🔗

@@ -1,41 +1,127 @@
-name: Publish zed-extension CLI
-
+# Generated from xtask::workflows::publish_extension_cli
+# Rebuild with `cargo xtask workflows`.
+name: publish_extension_cli
+env:
+  CARGO_TERM_COLOR: always
+  CARGO_INCREMENTAL: '0'
 on:
   push:
     tags:
-      - extension-cli
-
-env:
-  CARGO_TERM_COLOR: always
-  CARGO_INCREMENTAL: 0
-
+    - extension-cli
 jobs:
-  publish:
-    name: Publish zed-extension CLI
-    if: github.repository_owner == 'zed-industries'
-    runs-on:
-      - ubuntu-latest
+  publish_job:
+    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
+    runs-on: namespace-profile-2x4-ubuntu-2404
     steps:
-      - name: Checkout repo
-        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
-        with:
-          clean: false
-
-      - name: Cache dependencies
-        uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
-        with:
-          save-if: ${{ github.ref == 'refs/heads/main' }}
-          cache-provider: "github"
-
-      - name: Configure linux
-        shell: bash -euxo pipefail {0}
-        run: script/linux
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::cache_rust_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        cache: rust
+        path: ~/.rustup
+    - name: steps::setup_linux
+      run: ./script/linux
+    - name: publish_extension_cli::publish_job::build_extension_cli
+      run: cargo build --release --package extension_cli
+    - name: publish_extension_cli::publish_job::upload_binary
+      run: script/upload-extension-cli ${{ github.sha }}
+      env:
+        DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
+        DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
+  update_sha_in_zed:
+    needs:
+    - publish_job
+    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
+    runs-on: namespace-profile-8x16-ubuntu-2204
+    steps:
+    - id: generate-token
+      name: extension_bump::generate_token
+      uses: actions/create-github-app-token@v2
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - name: steps::cache_rust_dependencies_namespace
+      uses: namespacelabs/nscloud-cache-action@v1
+      with:
+        cache: rust
+        path: ~/.rustup
+    - id: short-sha
+      name: publish_extension_cli::get_short_sha
+      run: |
+        echo "sha_short=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
+    - name: publish_extension_cli::update_sha_in_zed::replace_sha
+      run: |
+        sed -i "s/ZED_EXTENSION_CLI_SHA: &str = \"[a-f0-9]*\"/ZED_EXTENSION_CLI_SHA: \&str = \"${{ github.sha }}\"/" \
+            tooling/xtask/src/tasks/workflows/extension_tests.rs
+    - name: publish_extension_cli::update_sha_in_zed::regenerate_workflows
+      run: cargo xtask workflows
+    - name: publish_extension_cli::create_pull_request_zed
+      uses: peter-evans/create-pull-request@v7
+      with:
+        title: 'extension_ci: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`'
+        body: |
+          This PR bumps the extension CLI version used in the extension workflows to `${{ github.sha }}`.
 
-      - name: Build extension CLI
-        run: cargo build --release --package extension_cli
+          Release Notes:
 
-      - name: Upload binary
-        env:
-          DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
-          DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
-        run: script/upload-extension-cli ${{ github.sha }}
+          - N/A
+        commit-message: 'extension_ci: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`'
+        branch: update-extension-cli-sha
+        committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
+        base: main
+        delete-branch: true
+        token: ${{ steps.generate-token.outputs.token }}
+        sign-commits: true
+        assignees: ${{ github.actor }}
+  update_sha_in_extensions:
+    needs:
+    - publish_job
+    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
+    runs-on: namespace-profile-2x4-ubuntu-2404
+    steps:
+    - id: generate-token
+      name: extension_bump::generate_token
+      uses: actions/create-github-app-token@v2
+      with:
+        app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+        private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+        owner: zed-industries
+        repositories: extensions
+    - id: short-sha
+      name: publish_extension_cli::get_short_sha
+      run: |
+        echo "sha_short=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
+    - name: publish_extension_cli::update_sha_in_extensions::checkout_extensions_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        repository: zed-industries/extensions
+        token: ${{ steps.generate-token.outputs.token }}
+    - name: publish_extension_cli::update_sha_in_extensions::replace_sha
+      run: |
+        sed -i "s/ZED_EXTENSION_CLI_SHA: [a-f0-9]*/ZED_EXTENSION_CLI_SHA: ${{ github.sha }}/" \
+            .github/workflows/ci.yml
+    - name: publish_extension_cli::create_pull_request_extensions
+      uses: peter-evans/create-pull-request@v7
+      with:
+        title: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`
+        body: |
+          This PR bumps the extension CLI version to https://github.com/zed-industries/zed/commit/${{ github.sha }}.
+        commit-message: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`
+        branch: update-extension-cli-sha
+        committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
+        base: main
+        delete-branch: true
+        token: ${{ steps.generate-token.outputs.token }}
+        sign-commits: true
+        labels: allow-no-extension
+        assignees: ${{ github.actor }}
+defaults:
+  run:
+    shell: bash -euxo pipefail {0}

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

@@ -16,6 +16,7 @@ mod extension_tests;
 mod extension_workflow_rollout;
 mod extensions;
 mod nix_build;
+mod publish_extension_cli;
 mod release_nightly;
 mod run_bundling;
 
@@ -137,6 +138,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
         WorkflowFile::zed(extension_release::extension_release),
         WorkflowFile::zed(extension_tests::extension_tests),
         WorkflowFile::zed(extension_workflow_rollout::extension_workflow_rollout),
+        WorkflowFile::zed(publish_extension_cli::publish_extension_cli),
         WorkflowFile::zed(release::release),
         WorkflowFile::zed(release_nightly::release_nightly),
         WorkflowFile::zed(run_agent_evals::run_agent_evals),

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

@@ -0,0 +1,201 @@
+use gh_workflow::{ctx::Context, *};
+use indoc::indoc;
+
+use crate::tasks::workflows::{
+    extension_bump::{RepositoryTarget, generate_token},
+    runners,
+    steps::{self, CommonJobConditions, NamedJob, named},
+    vars::{self, StepOutput},
+};
+
+pub fn publish_extension_cli() -> Workflow {
+    let publish = publish_job();
+    let update_sha_in_zed = update_sha_in_zed(&publish);
+    let update_sha_in_extensions = update_sha_in_extensions(&publish);
+
+    named::workflow()
+        .on(Event::default().push(Push::default().tags(vec!["extension-cli".to_string()])))
+        .add_env(("CARGO_TERM_COLOR", "always"))
+        .add_env(("CARGO_INCREMENTAL", 0))
+        .add_job(publish.name, publish.job)
+        .add_job(update_sha_in_zed.name, update_sha_in_zed.job)
+        .add_job(update_sha_in_extensions.name, update_sha_in_extensions.job)
+}
+
+fn publish_job() -> NamedJob {
+    fn build_extension_cli() -> Step<Run> {
+        named::bash("cargo build --release --package extension_cli")
+    }
+
+    fn upload_binary() -> Step<Run> {
+        named::bash("script/upload-extension-cli ${{ github.sha }}")
+            .add_env((
+                "DIGITALOCEAN_SPACES_ACCESS_KEY",
+                vars::DIGITALOCEAN_SPACES_ACCESS_KEY,
+            ))
+            .add_env((
+                "DIGITALOCEAN_SPACES_SECRET_KEY",
+                vars::DIGITALOCEAN_SPACES_SECRET_KEY,
+            ))
+    }
+
+    named::job(
+        Job::default()
+            .with_repository_owner_guard()
+            .runs_on(runners::LINUX_SMALL)
+            .add_step(steps::checkout_repo())
+            .add_step(steps::cache_rust_dependencies_namespace())
+            .add_step(steps::setup_linux())
+            .add_step(build_extension_cli())
+            .add_step(upload_binary()),
+    )
+}
+
+fn update_sha_in_zed(publish_job: &NamedJob) -> NamedJob {
+    let (generate_token, generated_token) = generate_token(
+        vars::ZED_ZIPPY_APP_ID,
+        vars::ZED_ZIPPY_APP_PRIVATE_KEY,
+        Some(RepositoryTarget::current()),
+    );
+
+    fn replace_sha() -> Step<Run> {
+        named::bash(indoc! {r#"
+            sed -i "s/ZED_EXTENSION_CLI_SHA: &str = \"[a-f0-9]*\"/ZED_EXTENSION_CLI_SHA: \&str = \"${{ github.sha }}\"/" \
+                tooling/xtask/src/tasks/workflows/extension_tests.rs
+        "#})
+    }
+
+    fn regenerate_workflows() -> Step<Run> {
+        named::bash("cargo xtask workflows")
+    }
+
+    let (get_short_sha_step, short_sha) = get_short_sha();
+
+    named::job(
+        Job::default()
+            .with_repository_owner_guard()
+            .needs(vec![publish_job.name.clone()])
+            .runs_on(runners::LINUX_LARGE)
+            .add_step(generate_token)
+            .add_step(steps::checkout_repo())
+            .add_step(steps::cache_rust_dependencies_namespace())
+            .add_step(get_short_sha_step)
+            .add_step(replace_sha())
+            .add_step(regenerate_workflows())
+            .add_step(create_pull_request_zed(&generated_token, &short_sha)),
+    )
+}
+
+fn create_pull_request_zed(generated_token: &StepOutput, short_sha: &StepOutput) -> Step<Use> {
+    let title = format!(
+        "extension_ci: Bump extension CLI version to `{}`",
+        short_sha
+    );
+
+    named::uses("peter-evans", "create-pull-request", "v7").with(
+        Input::default()
+            .add("title", title.clone())
+            .add(
+                "body",
+                indoc! {r#"
+                    This PR bumps the extension CLI version used in the extension workflows to `${{ github.sha }}`.
+
+                    Release Notes:
+
+                    - N/A
+                "#},
+            )
+            .add("commit-message", title)
+            .add("branch", "update-extension-cli-sha")
+            .add(
+                "committer",
+                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
+            )
+            .add("base", "main")
+            .add("delete-branch", true)
+            .add("token", generated_token.to_string())
+            .add("sign-commits", true)
+            .add("assignees", Context::github().actor().to_string()),
+    )
+}
+
+fn update_sha_in_extensions(publish_job: &NamedJob) -> NamedJob {
+    let extensions_repo = RepositoryTarget::new("zed-industries", &["extensions"]);
+    let (generate_token, generated_token) = generate_token(
+        vars::ZED_ZIPPY_APP_ID,
+        vars::ZED_ZIPPY_APP_PRIVATE_KEY,
+        Some(extensions_repo),
+    );
+
+    fn checkout_extensions_repo(token: &StepOutput) -> Step<Use> {
+        named::uses(
+            "actions",
+            "checkout",
+            "11bd71901bbe5b1630ceea73d27597364c9af683", // v4
+        )
+        .add_with(("repository", "zed-industries/extensions"))
+        .add_with(("token", token.to_string()))
+    }
+
+    fn replace_sha() -> Step<Run> {
+        named::bash(indoc! {r#"
+            sed -i "s/ZED_EXTENSION_CLI_SHA: [a-f0-9]*/ZED_EXTENSION_CLI_SHA: ${{ github.sha }}/" \
+                .github/workflows/ci.yml
+        "#})
+    }
+
+    let (get_short_sha_step, short_sha) = get_short_sha();
+
+    named::job(
+        Job::default()
+            .with_repository_owner_guard()
+            .needs(vec![publish_job.name.clone()])
+            .runs_on(runners::LINUX_SMALL)
+            .add_step(generate_token)
+            .add_step(get_short_sha_step)
+            .add_step(checkout_extensions_repo(&generated_token))
+            .add_step(replace_sha())
+            .add_step(create_pull_request_extensions(&generated_token, &short_sha)),
+    )
+}
+
+fn create_pull_request_extensions(
+    generated_token: &StepOutput,
+    short_sha: &StepOutput,
+) -> Step<Use> {
+    let title = format!("Bump extension CLI version to `{}`", short_sha);
+
+    named::uses("peter-evans", "create-pull-request", "v7").with(
+        Input::default()
+            .add("title", title.clone())
+            .add(
+                "body",
+                indoc! {r#"
+                    This PR bumps the extension CLI version to https://github.com/zed-industries/zed/commit/${{ github.sha }}.
+                "#},
+            )
+            .add("commit-message", title)
+            .add("branch", "update-extension-cli-sha")
+            .add(
+                "committer",
+                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
+            )
+            .add("base", "main")
+            .add("delete-branch", true)
+            .add("token", generated_token.to_string())
+            .add("sign-commits", true)
+            .add("labels", "allow-no-extension")
+            .add("assignees", Context::github().actor().to_string()),
+    )
+}
+
+fn get_short_sha() -> (Step<Run>, StepOutput) {
+    let step = named::bash(indoc::indoc! {r#"
+        echo "sha_short=$(echo "${{ github.sha }}" | cut -c1-7)" >> "$GITHUB_OUTPUT"
+    "#})
+    .id("short-sha");
+
+    let step_output = vars::StepOutput::new(&step, "sha_short");
+
+    (step, step_output)
+}