Add more workflows for extension repositories (#43924)

Finn Evers created

This PR adds workflows to be used for CD in extension reposiories in the
`zed-extensions` organization and updates some of the existing ones with
minor improvemts.

Release Notes:

- N/A

Change summary

.github/workflows/extension_bump.yml                            |  30 
.github/workflows/extension_release.yml                         |  41 
.github/workflows/run_bundling.yml                              |  12 
Cargo.lock                                                      |   4 
Cargo.toml                                                      |   2 
extensions/workflows/bump_version.yml                           |  48 +
extensions/workflows/release_version.yml                        |  13 
extensions/workflows/run_tests.yml                              |  13 
tooling/xtask/src/tasks/workflows.rs                            | 137 ++
tooling/xtask/src/tasks/workflows/extension_bump.rs             |  75 
tooling/xtask/src/tasks/workflows/extension_release.rs          |  70 +
tooling/xtask/src/tasks/workflows/extensions/bump_version.rs    |  99 ++
tooling/xtask/src/tasks/workflows/extensions/mod.rs             |  24 
tooling/xtask/src/tasks/workflows/extensions/release_version.rs |  26 
tooling/xtask/src/tasks/workflows/extensions/run_tests.rs       |  25 
tooling/xtask/src/tasks/workflows/run_bundling.rs               |  10 
tooling/xtask/src/tasks/workflows/steps.rs                      |  16 
tooling/xtask/src/tasks/workflows/vars.rs                       |  14 
18 files changed, 566 insertions(+), 93 deletions(-)

Detailed changes

.github/workflows/extension_bump.yml 🔗

@@ -13,6 +13,10 @@ on:
         description: bump-type
         type: string
         default: patch
+      force-bump:
+        description: force-bump
+        required: true
+        type: boolean
     secrets:
       app-id:
         description: The app ID used to create the PR
@@ -56,17 +60,24 @@ jobs:
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       with:
         clean: false
-        fetch-depth: 10
+        fetch-depth: 0
     - id: compare-versions-check
       name: extension_bump::compare_versions
       run: |
         CURRENT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
+        PR_PARENT_SHA="${{ github.event.pull_request.head.sha }}"
 
-        git checkout "$(git log -1 --format=%H)"~1
+        if [[ -n "$PR_PARENT_SHA" ]]; then
+            git checkout "$PR_PARENT_SHA"
+        elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then
+            git checkout "$BRANCH_PARENT_SHA"
+        else
+            git checkout "$(git log -1 --format=%H)"~1
+        fi
 
-        PREV_COMMIT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
+        PARENT_COMMIT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
 
-        [[ "$CURRENT_VERSION" == "$PREV_COMMIT_VERSION" ]] && \
+        [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \
           echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \
           echo "needs_bump=false" >> "$GITHUB_OUTPUT"
 
@@ -80,7 +91,9 @@ jobs:
     needs:
     - check_extension
     - check_bump_needed
-    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && needs.check_bump_needed.outputs.needs_bump == 'true'
+    if: |-
+      (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
+      (inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true')
     runs-on: namespace-profile-8x16-ubuntu-2204
     steps:
     - id: generate-token
@@ -99,7 +112,7 @@ jobs:
     - id: bump-version
       name: extension_bump::bump_version
       run: |
-        OLD_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
+        OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}"
 
         cat <<EOF > .bumpversion.cfg
         [bumpversion]
@@ -117,7 +130,6 @@ jobs:
 
         rm .bumpversion.cfg
 
-        echo "old_version=${OLD_VERSION}" >> "$GITHUB_OUTPUT"
         echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
       shell: bash -euxo pipefail {0}
     - name: extension_bump::create_pull_request
@@ -126,7 +138,7 @@ jobs:
         title: Bump version to ${{ steps.bump-version.outputs.new_version }}
         body: This PR bumps the version of this extension to v${{ steps.bump-version.outputs.new_version }}
         commit-message: Bump version to v${{ steps.bump-version.outputs.new_version }}
-        branch: bump-from-${{ steps.bump-version.outputs.old_version }}
+        branch: zed-zippy-autobump
         committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
         base: main
         delete-branch: true
@@ -137,7 +149,7 @@ jobs:
     needs:
     - check_extension
     - check_bump_needed
-    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && needs.check_bump_needed.outputs.needs_bump == 'false'
+    if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
     runs-on: namespace-profile-8x16-ubuntu-2204
     steps:
     - id: generate-token

.github/workflows/extension_release.yml 🔗

@@ -0,0 +1,41 @@
+# Generated from xtask::workflows::extension_release
+# Rebuild with `cargo xtask workflows`.
+name: extension_release
+on:
+  workflow_call:
+    secrets:
+      app-id:
+        description: The app ID used to create the PR
+        required: true
+      app-secret:
+        description: The app secret for the corresponding app ID
+        required: true
+jobs:
+  create_release:
+    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.app-id }}
+        private-key: ${{ secrets.app-secret }}
+    - name: steps::checkout_repo
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      with:
+        clean: false
+    - id: get-extension-id
+      name: extension_release::get_extension_id
+      run: |
+        EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)"
+
+        echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
+      shell: bash -euxo pipefail {0}
+    - name: extension_release::release_action
+      uses: huacnlee/zed-extension-action@v2
+      with:
+        extension-name: ${{ steps.get-extension-id.outputs.extension_id }}
+        push-to: zed-industries/extensions
+      env:
+        COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}

.github/workflows/run_bundling.yml 🔗

@@ -13,7 +13,7 @@ jobs:
   bundle_linux_aarch64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
     env:
       CARGO_INCREMENTAL: 0
@@ -56,7 +56,7 @@ jobs:
   bundle_linux_x86_64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: namespace-profile-32x64-ubuntu-2004
     env:
       CARGO_INCREMENTAL: 0
@@ -99,7 +99,7 @@ jobs:
   bundle_mac_aarch64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: self-mini-macos
     env:
       CARGO_INCREMENTAL: 0
@@ -145,7 +145,7 @@ jobs:
   bundle_mac_x86_64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: self-mini-macos
     env:
       CARGO_INCREMENTAL: 0
@@ -191,7 +191,7 @@ jobs:
   bundle_windows_aarch64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: self-32vcpu-windows-2022
     env:
       CARGO_INCREMENTAL: 0
@@ -229,7 +229,7 @@ jobs:
   bundle_windows_x86_64:
     if: |-
       (github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                       (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
+      (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
     runs-on: self-32vcpu-windows-2022
     env:
       CARGO_INCREMENTAL: 0

Cargo.lock 🔗

@@ -6969,7 +6969,7 @@ dependencies = [
 [[package]]
 name = "gh-workflow"
 version = "0.8.0"
-source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
+source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
 dependencies = [
  "async-trait",
  "derive_more 2.0.1",
@@ -6986,7 +6986,7 @@ dependencies = [
 [[package]]
 name = "gh-workflow-macros"
 version = "0.8.0"
-source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
+source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
 dependencies = [
  "heck 0.5.0",
  "quote",

Cargo.toml 🔗

@@ -508,7 +508,7 @@ fork = "0.4.0"
 futures = "0.3"
 futures-batch = "0.6.1"
 futures-lite = "1.13"
-gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "3eaa84abca0778eb54272f45a312cb24f9a0b435" }
+gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "e5f883040530b4df36437f140084ee5cc7c1c9be" }
 git2 = { version = "0.20.1", default-features = false }
 globset = "0.4"
 handlebars = "4.3"

extensions/workflows/bump_version.yml 🔗

@@ -0,0 +1,48 @@
+# Generated from xtask::workflows:: within the Zed repository.extensions::bump_version
+# Rebuild with `cargo xtask workflows`.
+name: extensions::bump_version
+on:
+  pull_request:
+    types:
+    - labeled
+  push:
+    branches:
+    - main
+jobs:
+  determine_bump_type:
+    runs-on: namespace-profile-16x32-ubuntu-2204
+    steps:
+    - id: get-bump-type
+      name: extensions::bump_version::get_bump_type
+      run: |
+        if [ "$HAS_MAJOR_LABEL" = "true" ]; then
+            bump_type="major"
+        elif [ "$HAS_MINOR_LABEL" = "true" ]; then
+            bump_type="minor"
+        else
+            bump_type="patch"
+        fi
+        echo "bump_type=$bump_type" >> $GITHUB_OUTPUT
+      shell: bash -euxo pipefail {0}
+      env:
+        HAS_MAJOR_LABEL: |-
+          ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') ||
+          (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }}
+        HAS_MINOR_LABEL: |-
+          ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') ||
+          (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }}
+    outputs:
+      bump_type: ${{ steps.get-bump-type.outputs.bump_type }}
+  call_bump_version:
+    needs:
+    - determine_bump_type
+    if: |-
+      (github.event.action == 'labeled' && needs.determine_bump_type.outputs.bump_type != 'patch') ||
+      github.event_name == 'push'
+    uses: zed-industries/zed/.github/workflows/extension_bump.yml@main
+    secrets:
+      app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+      app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
+    with:
+      bump-type: ${{ needs.determine_bump_type.outputs.bump_type }}
+      force-bump: true

extensions/workflows/release_version.yml 🔗

@@ -0,0 +1,13 @@
+# Generated from xtask::workflows:: within the Zed repository.extensions::release_version
+# Rebuild with `cargo xtask workflows`.
+name: extensions::release_version
+on:
+  push:
+    tags:
+    - v**
+jobs:
+  call_release_version:
+    uses: zed-industries/zed/.github/workflows/extension_release.yml@main
+    secrets:
+      app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
+      app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}

extensions/workflows/run_tests.yml 🔗

@@ -0,0 +1,13 @@
+# Generated from xtask::workflows:: within the Zed repository.extensions::run_tests
+# Rebuild with `cargo xtask workflows`.
+name: extensions::run_tests
+on:
+  pull_request:
+    branches:
+    - '**'
+jobs:
+  call_extension_tests:
+    uses: zed-industries/zed/.github/workflows/extension_tests.yml@main
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
+  cancel-in-progress: true

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

@@ -1,14 +1,17 @@
 use anyhow::{Context, Result};
 use clap::Parser;
+use gh_workflow::Workflow;
 use std::fs;
-use std::path::Path;
+use std::path::{Path, PathBuf};
 
 mod after_release;
 mod cherry_pick;
 mod compare_perf;
 mod danger;
 mod extension_bump;
+mod extension_release;
 mod extension_tests;
+mod extensions;
 mod nix_build;
 mod release_nightly;
 mod run_bundling;
@@ -23,44 +26,112 @@ mod vars;
 #[derive(Parser)]
 pub struct GenerateWorkflowArgs {}
 
+struct WorkflowFile {
+    source: fn() -> Workflow,
+    r#type: WorkflowType,
+}
+
+impl WorkflowFile {
+    fn zed(f: fn() -> Workflow) -> WorkflowFile {
+        WorkflowFile {
+            source: f,
+            r#type: WorkflowType::Zed,
+        }
+    }
+    fn extension(f: fn() -> Workflow) -> WorkflowFile {
+        WorkflowFile {
+            source: f,
+            r#type: WorkflowType::Extensions,
+        }
+    }
+
+    fn generate_file(&self) -> Result<()> {
+        let workflow = (self.source)();
+        let workflow_folder = self.r#type.folder_path();
+        let workflow_name = workflow
+            .name
+            .as_ref()
+            .expect("Workflow must have a name at this point");
+        let filename = format!(
+            "{}.yml",
+            workflow_name.rsplit("::").next().unwrap_or(workflow_name)
+        );
+
+        let workflow_path = workflow_folder.join(filename);
+
+        let content = workflow
+            .to_string()
+            .map_err(|e| anyhow::anyhow!("{:?}: {:?}", workflow_path, e))?;
+
+        let disclaimer = self.r#type.disclaimer(workflow_name);
+
+        let content = [disclaimer, content].join("\n");
+        fs::write(&workflow_path, content).map_err(Into::into)
+    }
+}
+
+enum WorkflowType {
+    Zed,
+    Extensions,
+}
+
+impl WorkflowType {
+    fn disclaimer(&self, workflow_name: &str) -> String {
+        format!(
+            concat!(
+                "# Generated from xtask::workflows::{}{}\n",
+                "# Rebuild with `cargo xtask workflows`.",
+            ),
+            matches!(self, WorkflowType::Extensions)
+                .then_some(" within the Zed repository.")
+                .unwrap_or_default(),
+            workflow_name
+        )
+    }
+
+    fn folder_path(&self) -> PathBuf {
+        match self {
+            WorkflowType::Zed => PathBuf::from(".github/workflows"),
+            WorkflowType::Extensions => PathBuf::from("extensions/workflows"),
+        }
+    }
+}
+
 pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> {
     if !Path::new("crates/zed/").is_dir() {
         anyhow::bail!("xtask workflows must be ran from the project root");
     }
-    let dir = Path::new(".github/workflows");
-
-    let workflows = vec![
-        ("danger.yml", danger::danger()),
-        ("run_bundling.yml", run_bundling::run_bundling()),
-        ("release_nightly.yml", release_nightly::release_nightly()),
-        ("run_tests.yml", run_tests::run_tests()),
-        ("release.yml", release::release()),
-        ("cherry_pick.yml", cherry_pick::cherry_pick()),
-        ("compare_perf.yml", compare_perf::compare_perf()),
-        ("run_unit_evals.yml", run_agent_evals::run_unit_evals()),
-        (
-            "run_cron_unit_evals.yml",
-            run_agent_evals::run_cron_unit_evals(),
-        ),
-        ("run_agent_evals.yml", run_agent_evals::run_agent_evals()),
-        ("after_release.yml", after_release::after_release()),
-        ("extension_tests.yml", extension_tests::extension_tests()),
-        ("extension_bump.yml", extension_bump::extension_bump()),
+    let workflow_dir = Path::new(".github/workflows");
+    let extension_workflow_dir = Path::new("extensions/workflows");
+
+    let workflows = [
+        WorkflowFile::zed(danger::danger),
+        WorkflowFile::zed(run_bundling::run_bundling),
+        WorkflowFile::zed(release_nightly::release_nightly),
+        WorkflowFile::zed(run_tests::run_tests),
+        WorkflowFile::zed(release::release),
+        WorkflowFile::zed(cherry_pick::cherry_pick),
+        WorkflowFile::zed(compare_perf::compare_perf),
+        WorkflowFile::zed(run_agent_evals::run_unit_evals),
+        WorkflowFile::zed(run_agent_evals::run_cron_unit_evals),
+        WorkflowFile::zed(run_agent_evals::run_agent_evals),
+        WorkflowFile::zed(after_release::after_release),
+        WorkflowFile::zed(extension_tests::extension_tests),
+        WorkflowFile::zed(extension_bump::extension_bump),
+        WorkflowFile::zed(extension_release::extension_release),
+        /* workflows used for CI/CD in extension repositories */
+        WorkflowFile::extension(extensions::run_tests::run_tests),
+        WorkflowFile::extension(extensions::bump_version::bump_version),
+        WorkflowFile::extension(extensions::release_version::release_version),
     ];
-    fs::create_dir_all(dir)
-        .with_context(|| format!("Failed to create directory: {}", dir.display()))?;
 
-    for (filename, workflow) in workflows {
-        let content = workflow
-            .to_string()
-            .map_err(|e| anyhow::anyhow!("{}: {:?}", filename, e))?;
-        let content = format!(
-            "# Generated from xtask::workflows::{}\n# Rebuild with `cargo xtask workflows`.\n{}",
-            workflow.name.unwrap(),
-            content
-        );
-        let file_path = dir.join(filename);
-        fs::write(&file_path, content)?;
+    for directory in [&workflow_dir, &extension_workflow_dir] {
+        fs::create_dir_all(directory)
+            .with_context(|| format!("Failed to create directory: {}", directory.display()))?;
+    }
+
+    for workflow_file in workflows {
+        workflow_file.generate_file()?;
     }
 
     Ok(())

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

@@ -2,6 +2,7 @@ use gh_workflow::*;
 use indoc::indoc;
 
 use crate::tasks::workflows::{
+    extension_release::extension_workflow_secrets,
     extension_tests::{self},
     runners,
     steps::{self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob, named},
@@ -25,10 +26,11 @@ const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.
 // This is used by various extensions repos in the zed-extensions org to bump extension versions.
 pub(crate) fn extension_bump() -> Workflow {
     let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned()));
+    // TODO: Ideally, this would have a default of `false`, but this is currently not
+    // supported in gh-workflows
+    let force_bump = WorkflowInput::bool("force-bump", None);
 
-    let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR");
-    let app_secret =
-        WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID");
+    let (app_id, app_secret) = extension_workflow_secrets();
 
     let test_extension = extension_tests::check_extension();
     let (check_bump_needed, needs_bump, current_version) = check_bump_needed();
@@ -38,8 +40,15 @@ pub(crate) fn extension_bump() -> Workflow {
 
     let dependencies = [&test_extension, &check_bump_needed];
 
-    let bump_version =
-        bump_extension_version(&dependencies, &bump_type, &needs_bump, &app_id, &app_secret);
+    let bump_version = bump_extension_version(
+        &dependencies,
+        &current_version,
+        &bump_type,
+        &needs_bump,
+        &force_bump,
+        &app_id,
+        &app_secret,
+    );
     let create_label = create_version_label(
         &dependencies,
         &needs_bump,
@@ -53,6 +62,7 @@ pub(crate) fn extension_bump() -> Workflow {
             Event::default().workflow_call(
                 WorkflowCall::default()
                     .add_input(bump_type.name, bump_type.call_input())
+                    .add_input(force_bump.name, force_bump.call_input())
                     .secrets([
                         (app_id.name.to_owned(), app_id.secret_configuration()),
                         (
@@ -90,7 +100,7 @@ fn check_bump_needed() -> (NamedJob, StepOutput, StepOutput) {
         ])
         .runs_on(runners::LINUX_SMALL)
         .timeout_minutes(1u32)
-        .add_step(steps::checkout_repo().add_with(("fetch-depth", 10)))
+        .add_step(steps::checkout_repo().add_with(("fetch-depth", 0)))
         .add_step(compare_versions);
 
     (named::job(job), version_changed, current_version)
@@ -106,7 +116,7 @@ fn create_version_label(
     let (generate_token, generated_token) = generate_token(app_id, app_secret);
     let job = steps::dependant_job(dependencies)
         .cond(Expression::new(format!(
-            "{DEFAULT_REPOSITORY_OWNER_GUARD} && {} == 'false'",
+            "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'",
             needs_bump.expr(),
         )))
         .runs_on(runners::LINUX_LARGE)
@@ -143,14 +153,21 @@ fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput)
 fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
     let check_needs_bump = named::bash(format!(
         indoc! {
-            r#"
+        r#"
         CURRENT_VERSION="$({})"
+        PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}"
 
-        git checkout "$(git log -1 --format=%H)"~1
+        if [[ -n "$PR_PARENT_SHA" ]]; then
+            git checkout "$PR_PARENT_SHA"
+        elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then
+            git checkout "$BRANCH_PARENT_SHA"
+        else
+            git checkout "$(git log -1 --format=%H)"~1
+        fi
 
-        PREV_COMMIT_VERSION="$({})"
+        PARENT_COMMIT_VERSION="$({})"
 
-        [[ "$CURRENT_VERSION" == "$PREV_COMMIT_VERSION" ]] && \
+        [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \
           echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \
           echo "needs_bump=false" >> "$GITHUB_OUTPUT"
 
@@ -169,17 +186,20 @@ fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
 
 fn bump_extension_version(
     dependencies: &[&NamedJob],
+    current_version: &JobOutput,
     bump_type: &WorkflowInput,
     needs_bump: &JobOutput,
+    force_bump: &WorkflowInput,
     app_id: &WorkflowSecret,
     app_secret: &WorkflowSecret,
 ) -> NamedJob {
     let (generate_token, generated_token) = generate_token(app_id, app_secret);
-    let (bump_version, old_version, new_version) = bump_version(bump_type);
+    let (bump_version, new_version) = bump_version(current_version, bump_type);
 
     let job = steps::dependant_job(dependencies)
         .cond(Expression::new(format!(
-            "{DEFAULT_REPOSITORY_OWNER_GUARD} && {} == 'true'",
+            "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({} == 'true' || {} == 'true')",
+            force_bump.expr(),
             needs_bump.expr(),
         )))
         .runs_on(runners::LINUX_LARGE)
@@ -188,16 +208,15 @@ fn bump_extension_version(
         .add_step(steps::checkout_repo())
         .add_step(install_bump_2_version())
         .add_step(bump_version)
-        .add_step(create_pull_request(
-            old_version,
-            new_version,
-            generated_token,
-        ));
+        .add_step(create_pull_request(new_version, generated_token));
 
     named::job(job)
 }
 
-fn generate_token(app_id: &WorkflowSecret, app_secret: &WorkflowSecret) -> (Step<Use>, StepOutput) {
+pub(crate) fn generate_token(
+    app_id: &WorkflowSecret,
+    app_secret: &WorkflowSecret,
+) -> (Step<Use>, StepOutput) {
     let step = named::uses("actions", "create-github-app-token", "v2")
         .id("generate-token")
         .add_with(
@@ -215,10 +234,10 @@ fn install_bump_2_version() -> Step<Run> {
     named::run(runners::Platform::Linux, "pip install bump2version")
 }
 
-fn bump_version(bump_type: &WorkflowInput) -> (Step<Run>, StepOutput, StepOutput) {
+fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step<Run>, StepOutput) {
     let step = named::bash(format!(
         indoc! {r#"
-            OLD_VERSION="$({})"
+            OLD_VERSION="{}"
 
             cat <<EOF > .bumpversion.cfg
             {}
@@ -230,24 +249,18 @@ fn bump_version(bump_type: &WorkflowInput) -> (Step<Run>, StepOutput, StepOutput
 
             rm .bumpversion.cfg
 
-            echo "old_version=${{OLD_VERSION}}" >> "$GITHUB_OUTPUT"
             echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT"
             "#
         },
-        VERSION_CHECK, BUMPVERSION_CONFIG, bump_type, VERSION_CHECK
+        current_version, BUMPVERSION_CONFIG, bump_type, VERSION_CHECK
     ))
     .id("bump-version");
 
-    let old_version = StepOutput::new(&step, "old_version");
     let new_version = StepOutput::new(&step, "new_version");
-    (step, old_version, new_version)
+    (step, new_version)
 }
 
-fn create_pull_request(
-    old_version: StepOutput,
-    new_version: StepOutput,
-    generated_token: StepOutput,
-) -> Step<Use> {
+fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step<Use> {
     let formatted_version = format!("v{}", new_version);
 
     named::uses("peter-evans", "create-pull-request", "v7").with(
@@ -264,7 +277,7 @@ fn create_pull_request(
                 "commit-message",
                 format!("Bump version to {}", formatted_version),
             )
-            .add("branch", format!("bump-from-{}", old_version))
+            .add("branch", "zed-zippy-autobump")
             .add(
                 "committer",
                 "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",

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

@@ -0,0 +1,70 @@
+use gh_workflow::{Event, Job, Run, Step, Use, Workflow, WorkflowCall};
+use indoc::indoc;
+
+use crate::tasks::workflows::{
+    extension_bump::generate_token,
+    runners,
+    steps::{CommonJobConditions, NamedJob, checkout_repo, named},
+    vars::{StepOutput, WorkflowSecret},
+};
+
+pub(crate) fn extension_release() -> Workflow {
+    let (app_id, app_secret) = extension_workflow_secrets();
+
+    let create_release = create_release(&app_id, &app_secret);
+    named::workflow()
+        .on(
+            Event::default().workflow_call(WorkflowCall::default().secrets([
+                (app_id.name.to_owned(), app_id.secret_configuration()),
+                (
+                    app_secret.name.to_owned(),
+                    app_secret.secret_configuration(),
+                ),
+            ])),
+        )
+        .add_job(create_release.name, create_release.job)
+}
+
+fn create_release(app_id: &WorkflowSecret, app_secret: &WorkflowSecret) -> NamedJob {
+    let (generate_token, generated_token) = generate_token(&app_id, &app_secret);
+    let (get_extension_id, extension_id) = get_extension_id();
+
+    let job = Job::default()
+        .with_repository_owner_guard()
+        .runs_on(runners::LINUX_LARGE)
+        .add_step(generate_token)
+        .add_step(checkout_repo())
+        .add_step(get_extension_id)
+        .add_step(release_action(extension_id, generated_token));
+
+    named::job(job)
+}
+
+fn get_extension_id() -> (Step<Run>, StepOutput) {
+    let step = named::bash(indoc! {
+    r#"
+        EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)"
+
+        echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
+    "#})
+    .id("get-extension-id");
+
+    let extension_id = StepOutput::new(&step, "extension_id");
+
+    (step, extension_id)
+}
+
+fn release_action(extension_id: StepOutput, generated_token: StepOutput) -> Step<Use> {
+    named::uses("huacnlee", "zed-extension-action", "v2")
+        .add_with(("extension-name", extension_id.to_string()))
+        .add_with(("push-to", "zed-industries/extensions"))
+        .add_env(("COMMITTER_TOKEN", generated_token.to_string()))
+}
+
+pub(crate) fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) {
+    let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR");
+    let app_secret =
+        WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID");
+
+    (app_id, app_secret)
+}

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

@@ -0,0 +1,99 @@
+use gh_workflow::{
+    Event, Expression, Input, Job, PullRequest, PullRequestType, Push, Run, Step, UsesJob, Workflow,
+};
+use indexmap::IndexMap;
+use indoc::indoc;
+
+use crate::tasks::workflows::{
+    runners,
+    steps::{NamedJob, named},
+    vars::{self, JobOutput, StepOutput},
+};
+
+pub(crate) fn bump_version() -> Workflow {
+    let (determine_bump_type, bump_type) = determine_bump_type();
+    let bump_type = bump_type.as_job_output(&determine_bump_type);
+
+    let call_bump_version = call_bump_version(&determine_bump_type, bump_type);
+
+    named::workflow()
+        .on(Event::default()
+            .push(Push::default().add_branch("main"))
+            .pull_request(PullRequest::default().add_type(PullRequestType::Labeled)))
+        .add_job(determine_bump_type.name, determine_bump_type.job)
+        .add_job(call_bump_version.name, call_bump_version.job)
+}
+
+pub(crate) fn call_bump_version(
+    depending_job: &NamedJob,
+    bump_type: JobOutput,
+) -> NamedJob<UsesJob> {
+    let job = Job::default()
+        .cond(Expression::new(format!(
+            indoc! {
+                "(github.event.action == 'labeled' && {} != 'patch') ||
+                github.event_name == 'push'"
+            },
+            bump_type.expr()
+        )))
+        .uses(
+            "zed-industries",
+            "zed",
+            ".github/workflows/extension_bump.yml",
+            "main",
+        )
+        .add_need(depending_job.name.clone())
+        .with(
+            Input::default()
+                .add("bump-type", bump_type.to_string())
+                .add("force-bump", true),
+        )
+        .secrets(IndexMap::from([
+            ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()),
+            (
+                "app-secret".to_owned(),
+                vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(),
+            ),
+        ]));
+
+    named::job(job)
+}
+
+fn determine_bump_type() -> (NamedJob, StepOutput) {
+    let (get_bump_type, output) = get_bump_type();
+    let job = Job::default()
+        .runs_on(runners::LINUX_DEFAULT)
+        .add_step(get_bump_type)
+        .outputs([(output.name.to_owned(), output.to_string())]);
+    (named::job(job), output)
+}
+
+fn get_bump_type() -> (Step<Run>, StepOutput) {
+    let step = named::bash(
+        indoc! {r#"
+            if [ "$HAS_MAJOR_LABEL" = "true" ]; then
+                bump_type="major"
+            elif [ "$HAS_MINOR_LABEL" = "true" ]; then
+                bump_type="minor"
+            else
+                bump_type="patch"
+            fi
+            echo "bump_type=$bump_type" >> $GITHUB_OUTPUT
+        "#},
+    )
+    .add_env(("HAS_MAJOR_LABEL",
+        indoc!{
+            "${{ (github.event.action == 'labeled' && github.event.label.name == 'major') ||
+            (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }}"
+        }))
+    .add_env(("HAS_MINOR_LABEL",
+        indoc!{
+            "${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') ||
+            (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }}"
+        }))
+    .id("get-bump-type");
+
+    let step_output = StepOutput::new(&step, "bump_type");
+
+    (step, step_output)
+}

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

@@ -0,0 +1,24 @@
+use gh_workflow::{Job, UsesJob};
+use indexmap::IndexMap;
+
+use crate::tasks::workflows::vars;
+
+pub(crate) mod bump_version;
+pub(crate) mod release_version;
+pub(crate) mod run_tests;
+
+pub(crate) trait WithAppSecrets: Sized {
+    fn with_app_secrets(self) -> Self;
+}
+
+impl WithAppSecrets for Job<UsesJob> {
+    fn with_app_secrets(self) -> Self {
+        self.secrets(IndexMap::from([
+            ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()),
+            (
+                "app-secret".to_owned(),
+                vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(),
+            ),
+        ]))
+    }
+}

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

@@ -0,0 +1,26 @@
+use gh_workflow::{Event, Job, Push, UsesJob, Workflow};
+
+use crate::tasks::workflows::{
+    extensions::WithAppSecrets,
+    steps::{NamedJob, named},
+};
+
+pub(crate) fn release_version() -> Workflow {
+    let create_release = call_release_version();
+    named::workflow()
+        .on(Event::default().push(Push::default().add_tag("v**")))
+        .add_job(create_release.name, create_release.job)
+}
+
+pub(crate) fn call_release_version() -> NamedJob<UsesJob> {
+    let job = Job::default()
+        .uses(
+            "zed-industries",
+            "zed",
+            ".github/workflows/extension_release.yml",
+            "main",
+        )
+        .with_app_secrets();
+
+    named::job(job)
+}

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

@@ -0,0 +1,25 @@
+use gh_workflow::{Event, Job, PullRequest, UsesJob, Workflow};
+
+use crate::tasks::workflows::{
+    steps::{NamedJob, named},
+    vars::one_workflow_per_non_main_branch,
+};
+
+pub(crate) fn run_tests() -> Workflow {
+    let call_extension_tests = call_extension_tests();
+    named::workflow()
+        .on(Event::default().pull_request(PullRequest::default().add_branch("**")))
+        .concurrency(one_workflow_per_non_main_branch())
+        .add_job(call_extension_tests.name, call_extension_tests.job)
+}
+
+pub(crate) fn call_extension_tests() -> NamedJob<UsesJob> {
+    let job = Job::default().uses(
+        "zed-industries",
+        "zed",
+        ".github/workflows/extension_tests.yml",
+        "main",
+    );
+
+    named::job(job)
+}

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

@@ -9,6 +9,7 @@ use crate::tasks::workflows::{
 
 use super::{runners, steps};
 use gh_workflow::*;
+use indoc::indoc;
 
 pub fn run_bundling() -> Workflow {
     let bundle = ReleaseBundleJobs {
@@ -42,10 +43,11 @@ pub fn run_bundling() -> Workflow {
 fn bundle_job(deps: &[&NamedJob]) -> Job {
     dependant_job(deps)
         .when(deps.len() == 0, |job|
-                job.cond(Expression::new(
-                "(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
-                 (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))",
-            )))
+            job.cond(Expression::new(
+                indoc! {
+                    r#"(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
+                    (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))"#,
+                })))
         .timeout_minutes(60u32)
 }
 

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

@@ -128,9 +128,9 @@ pub fn script(name: &str) -> Step<Run> {
     }
 }
 
-pub struct NamedJob {
+pub struct NamedJob<J: JobType = RunJob> {
     pub name: String,
-    pub job: Job,
+    pub job: Job<J>,
 }
 
 // impl NamedJob {
@@ -282,15 +282,19 @@ pub mod named {
         Workflow::default().name(
             named::function_name(1)
                 .split("::")
-                .next()
-                .unwrap()
-                .to_owned(),
+                .collect::<Vec<_>>()
+                .into_iter()
+                .rev()
+                .skip(1)
+                .rev()
+                .collect::<Vec<_>>()
+                .join("::"),
         )
     }
 
     /// Returns a Job with the same name as the enclosing function.
     /// (note job names may not contain `::`)
-    pub fn job(job: Job) -> NamedJob {
+    pub fn job<J: JobType>(job: Job<J>) -> NamedJob<J> {
         NamedJob {
             name: function_name(1).split("::").last().unwrap().to_owned(),
             job,

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

@@ -219,6 +219,14 @@ impl WorkflowInput {
         }
     }
 
+    pub fn bool(name: &'static str, default: Option<bool>) -> Self {
+        Self {
+            input_type: "boolean",
+            name,
+            default: default.as_ref().map(ToString::to_string),
+        }
+    }
+
     pub fn input(&self) -> WorkflowDispatchInput {
         WorkflowDispatchInput {
             description: self.name.to_owned(),
@@ -236,11 +244,15 @@ impl WorkflowInput {
             default: self.default.clone(),
         }
     }
+
+    pub(crate) fn expr(&self) -> String {
+        format!("inputs.{}", self.name)
+    }
 }
 
 impl std::fmt::Display for WorkflowInput {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "${{{{ inputs.{} }}}}", self.name)
+        write!(f, "${{{{ {} }}}}", self.expr())
     }
 }