extension_ci: Add initial support for extensions in a subdirectory (#51173)

Finn Evers created

This wil help with releases for extensions living this repository, which
will become more relevant once agent provider extensions are back on the
table.

Release Notes:

- N/A

Change summary

.github/workflows/extension_bump.yml                            | 47 +
.github/workflows/extension_tests.yml                           | 63 +
.github/workflows/run_tests.yml                                 |  4 
tooling/xtask/src/tasks/workflows/extension_bump.rs             | 82 +
tooling/xtask/src/tasks/workflows/extension_tests.rs            | 93 ++
tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs |  5 
tooling/xtask/src/tasks/workflows/run_tests.rs                  | 32 
tooling/xtask/src/tasks/workflows/steps.rs                      |  9 
tooling/xtask/src/tasks/workflows/vars.rs                       | 33 
9 files changed, 278 insertions(+), 90 deletions(-)

Detailed changes

.github/workflows/extension_bump.yml 🔗

@@ -17,6 +17,10 @@ on:
         description: force-bump
         required: true
         type: boolean
+      working-directory:
+        description: working-directory
+        type: string
+        default: .
     secrets:
       app-id:
         description: The app ID used to create the PR
@@ -42,8 +46,6 @@ jobs:
         if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
             PR_FORK_POINT="$(git merge-base origin/main HEAD)"
             git checkout "$PR_FORK_POINT"
-        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
@@ -59,6 +61,10 @@ jobs:
       version_changed: ${{ steps.compare-versions-check.outputs.version_changed }}
       current_version: ${{ steps.compare-versions-check.outputs.current_version }}
     timeout-minutes: 1
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   bump_extension_version:
     needs:
     - check_version_changed
@@ -98,18 +104,35 @@ jobs:
         fi
 
         NEW_VERSION="$(sed -n 's/^version = \"\(.*\)\"/\1/p' < extension.toml | tr -d '[:space:]')"
+        EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+        EXTENSION_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+
+        if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then
+            {
+                echo "title=Bump version to ${NEW_VERSION}";
+                echo "body=This PR bumps the version of this extension to v${NEW_VERSION}";
+                echo "branch_name=zed-zippy-autobump";
+            } >> "$GITHUB_OUTPUT"
+        else
+            {
+                echo "title=${EXTENSION_ID}: Bump to v${NEW_VERSION}";
+                echo "body=This PR bumps the version of the ${EXTENSION_NAME} extension to v${NEW_VERSION}";
+                echo "branch_name=zed-zippy-${EXTENSION_ID}-autobump";
+            } >> "$GITHUB_OUTPUT"
+        fi
 
         echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
       env:
         OLD_VERSION: ${{ needs.check_version_changed.outputs.current_version }}
         BUMP_TYPE: ${{ inputs.bump-type }}
+        WORKING_DIR: ${{ inputs.working-directory }}
     - name: extension_bump::create_pull_request
       uses: peter-evans/create-pull-request@v7
       with:
-        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: zed-zippy-autobump
+        title: ${{ steps.bump-version.outputs.title }}
+        body: ${{ steps.bump-version.outputs.body }}
+        commit-message: ${{ steps.bump-version.outputs.title }}
+        branch: ${{ steps.bump-version.outputs.branch_name }}
         committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
         base: main
         delete-branch: true
@@ -117,6 +140,10 @@ jobs:
         sign-commits: true
         assignees: ${{ github.actor }}
     timeout-minutes: 3
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   create_version_label:
     needs:
     - check_version_changed
@@ -145,6 +172,10 @@ jobs:
           })
         github-token: ${{ steps.generate-token.outputs.token }}
     timeout-minutes: 1
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   trigger_release:
     needs:
     - check_version_changed
@@ -178,6 +209,10 @@ jobs:
         tag: v${{ needs.check_version_changed.outputs.current_version }}
       env:
         COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
   cancel-in-progress: true

.github/workflows/extension_tests.yml 🔗

@@ -9,7 +9,12 @@ env:
   RUSTUP_TOOLCHAIN: stable
   CARGO_BUILD_TARGET: wasm32-wasip2
 on:
-  workflow_call: {}
+  workflow_call:
+    inputs:
+      working-directory:
+        description: working-directory
+        type: string
+        default: .
 jobs:
   orchestrate:
     if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -34,6 +39,14 @@ jobs:
         fi
         CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA")"
 
+        # When running from a subdirectory, git diff returns repo-root-relative paths.
+        # Filter to only files within the current working directory and strip the prefix.
+        REPO_SUBDIR="$(git rev-parse --show-prefix)"
+        REPO_SUBDIR="${REPO_SUBDIR%/}"
+        if [ -n "$REPO_SUBDIR" ]; then
+            CHANGED_FILES="$(echo "$CHANGED_FILES" | grep "^${REPO_SUBDIR}/" | sed "s|^${REPO_SUBDIR}/||" || true)"
+        fi
+
         check_pattern() {
           local output_name="$1"
           local pattern="$2"
@@ -49,6 +62,10 @@ jobs:
     outputs:
       check_rust: ${{ steps.filter.outputs.check_rust }}
       check_extension: ${{ steps.filter.outputs.check_extension }}
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   check_rust:
     needs:
     - orchestrate
@@ -66,17 +83,31 @@ jobs:
         path: ~/.rustup
     - name: extension_tests::install_rust_target
       run: rustup target add wasm32-wasip2
-    - name: steps::cargo_fmt
-      run: cargo fmt --all -- --check
+    - id: get-package-name
+      name: extension_tests::get_package_name
+      run: |
+        PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < Cargo.toml | head -1 | tr -d '[:space:]')"
+        echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
+    - name: extension_tests::cargo_fmt_package
+      run: cargo fmt -p "$PACKAGE_NAME" -- --check
+      env:
+        PACKAGE_NAME: ${{ steps.get-package-name.outputs.package_name }}
     - name: extension_tests::run_clippy
-      run: cargo clippy --release --all-features -- --deny warnings
+      run: cargo clippy -p "$PACKAGE_NAME" --release --all-features -- --deny warnings
+      env:
+        PACKAGE_NAME: ${{ steps.get-package-name.outputs.package_name }}
     - name: steps::cargo_install_nextest
       uses: taiki-e/install-action@nextest
-    - name: steps::cargo_nextest
-      run: 'cargo nextest run --workspace --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n ''s|host: ||p'')"'
+    - name: extension_tests::run_nextest
+      run: 'cargo nextest run -p "$PACKAGE_NAME" --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n ''s|host: ||p'')"'
       env:
+        PACKAGE_NAME: ${{ steps.get-package-name.outputs.package_name }}
         NEXTEST_NO_TESTS: warn
     timeout-minutes: 6
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   check_extension:
     needs:
     - orchestrate
@@ -97,8 +128,8 @@ jobs:
     - name: extension_tests::download_zed_extension_cli
       if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
       run: |
-        wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
-        chmod +x zed-extension
+        wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" -O "$GITHUB_WORKSPACE/zed-extension"
+        chmod +x "$GITHUB_WORKSPACE/zed-extension"
     - name: steps::cache_rust_dependencies_namespace
       uses: namespacelabs/nscloud-cache-action@v1
       with:
@@ -108,7 +139,7 @@ jobs:
       run: |
         mkdir -p /tmp/ext-scratch
         mkdir -p /tmp/ext-output
-        ./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
+        "$GITHUB_WORKSPACE/zed-extension" --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
     - name: run_tests::fetch_ts_query_ls
       uses: dsaltares/fetch-gh-release-asset@aa37ae5c44d3c9820bc12fe675e8670ecd93bd1c
       with:
@@ -117,8 +148,8 @@ jobs:
         file: ts_query_ls-x86_64-unknown-linux-gnu.tar.gz
     - name: run_tests::run_ts_query_ls
       run: |-
-        tar -xf ts_query_ls-x86_64-unknown-linux-gnu.tar.gz
-        ./ts_query_ls format --check . || {
+        tar -xf "$GITHUB_WORKSPACE/ts_query_ls-x86_64-unknown-linux-gnu.tar.gz" -C "$GITHUB_WORKSPACE"
+        "$GITHUB_WORKSPACE/ts_query_ls" format --check . || {
             echo "Found unformatted queries, please format them with ts_query_ls."
             echo "For easy use, install the Tree-sitter query extension:"
             echo "zed://extension/tree-sitter-query"
@@ -132,8 +163,6 @@ jobs:
         if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
             PR_FORK_POINT="$(git merge-base origin/main HEAD)"
             git checkout "$PR_FORK_POINT"
-        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
@@ -156,6 +185,10 @@ jobs:
         VERSION_CHANGED: ${{ steps.compare-versions-check.outputs.version_changed }}
         PR_USER_LOGIN: ${{ github.event.pull_request.user.login }}
     timeout-minutes: 6
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
   tests_pass:
     needs:
     - orchestrate
@@ -183,6 +216,10 @@ jobs:
         RESULT_ORCHESTRATE: ${{ needs.orchestrate.result }}
         RESULT_CHECK_RUST: ${{ needs.check_rust.result }}
         RESULT_CHECK_EXTENSION: ${{ needs.check_extension.result }}
+    defaults:
+      run:
+        shell: bash -euxo pipefail {0}
+        working-directory: ${{ inputs.working-directory }}
 concurrency:
   group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
   cancel-in-progress: true

.github/workflows/run_tests.yml 🔗

@@ -147,8 +147,8 @@ jobs:
         file: ts_query_ls-x86_64-unknown-linux-gnu.tar.gz
     - name: run_tests::run_ts_query_ls
       run: |-
-        tar -xf ts_query_ls-x86_64-unknown-linux-gnu.tar.gz
-        ./ts_query_ls format --check . || {
+        tar -xf "$GITHUB_WORKSPACE/ts_query_ls-x86_64-unknown-linux-gnu.tar.gz" -C "$GITHUB_WORKSPACE"
+        "$GITHUB_WORKSPACE/ts_query_ls" format --check . || {
             echo "Found unformatted queries, please format them with ts_query_ls."
             echo "For easy use, install the Tree-sitter query extension:"
             echo "zed://extension/tree-sitter-query"

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

@@ -5,8 +5,8 @@ use crate::tasks::workflows::{
     extension_tests::{self},
     runners,
     steps::{
-        self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob,
-        checkout_repo, dependant_job, named,
+        self, BASH_SHELL, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder,
+        NamedJob, checkout_repo, dependant_job, named,
     },
     vars::{
         JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch,
@@ -22,6 +22,7 @@ pub(crate) fn extension_bump() -> Workflow {
     // 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 working_directory = WorkflowInput::string("working-directory", Some(".".to_owned()));
 
     let (app_id, app_secret) = extension_workflow_secrets();
     let (check_version_changed, version_changed, current_version) = check_version_changed();
@@ -59,6 +60,7 @@ pub(crate) fn extension_bump() -> Workflow {
                 WorkflowCall::default()
                     .add_input(bump_type.name, bump_type.call_input())
                     .add_input(force_bump.name, force_bump.call_input())
+                    .add_input(working_directory.name, working_directory.call_input())
                     .secrets([
                         (app_id.name.to_owned(), app_id.secret_configuration()),
                         (
@@ -82,10 +84,19 @@ pub(crate) fn extension_bump() -> Workflow {
         .add_job(trigger_release.name, trigger_release.job)
 }
 
+fn extension_job_defaults() -> Defaults {
+    Defaults::default().run(
+        RunDefaults::default()
+            .shell(BASH_SHELL)
+            .working_directory("${{ inputs.working-directory }}"),
+    )
+}
+
 fn check_version_changed() -> (NamedJob, StepOutput, StepOutput) {
     let (compare_versions, version_changed, current_version) = compare_versions();
 
     let job = Job::default()
+        .defaults(extension_job_defaults())
         .with_repository_owner_guard()
         .outputs([
             (version_changed.name.to_owned(), version_changed.to_string()),
@@ -112,6 +123,7 @@ fn create_version_label(
     let (generate_token, generated_token) =
         generate_token(&app_id.to_string(), &app_secret.to_string(), None);
     let job = steps::dependant_job(dependencies)
+        .defaults(extension_job_defaults())
         .cond(Expression::new(format!(
             "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && \
             github.ref == 'refs/heads/main' && {version_changed} == 'true'",
@@ -153,8 +165,6 @@ pub(crate) fn compare_versions() -> (Step<Run>, StepOutput, StepOutput) {
         if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
             PR_FORK_POINT="$(git merge-base origin/main HEAD)"
             git checkout "$PR_FORK_POINT"
-        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
@@ -187,9 +197,11 @@ fn bump_extension_version(
 ) -> NamedJob {
     let (generate_token, generated_token) =
         generate_token(&app_id.to_string(), &app_secret.to_string(), None);
-    let (bump_version, new_version) = bump_version(current_version, bump_type);
+    let (bump_version, _new_version, title, body, branch_name) =
+        bump_version(current_version, bump_type);
 
     let job = steps::dependant_job(dependencies)
+        .defaults(extension_job_defaults())
         .cond(Expression::new(format!(
             "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({force_bump} == true || {version_changed} == 'false')",
             force_bump = force_bump_output.expr(),
@@ -201,7 +213,12 @@ fn bump_extension_version(
         .add_step(steps::checkout_repo())
         .add_step(install_bump_2_version())
         .add_step(bump_version)
-        .add_step(create_pull_request(new_version, generated_token));
+        .add_step(create_pull_request(
+            title,
+            body,
+            generated_token,
+            branch_name,
+        ));
 
     named::job(job)
 }
@@ -256,7 +273,10 @@ fn install_bump_2_version() -> Step<Run> {
     )
 }
 
-fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step<Run>, StepOutput) {
+fn bump_version(
+    current_version: &JobOutput,
+    bump_type: &WorkflowInput,
+) -> (Step<Run>, StepOutput, StepOutput, StepOutput, StepOutput) {
     let step = named::bash(formatdoc! {r#"
         BUMP_FILES=("extension.toml")
         if [[ -f "Cargo.toml" ]]; then
@@ -274,33 +294,50 @@ fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step
         fi
 
         NEW_VERSION="$({VERSION_CHECK})"
+        EXTENSION_ID="$(sed -n 's/^id = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+        EXTENSION_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < extension.toml | head -1 | tr -d '[:space:]')"
+
+        if [[ "$WORKING_DIR" == "." || -z "$WORKING_DIR" ]]; then
+            {{
+                echo "title=Bump version to ${{NEW_VERSION}}";
+                echo "body=This PR bumps the version of this extension to v${{NEW_VERSION}}";
+                echo "branch_name=zed-zippy-autobump";
+            }} >> "$GITHUB_OUTPUT"
+        else
+            {{
+                echo "title=${{EXTENSION_ID}}: Bump to v${{NEW_VERSION}}";
+                echo "body=This PR bumps the version of the ${{EXTENSION_NAME}} extension to v${{NEW_VERSION}}";
+                echo "branch_name=zed-zippy-${{EXTENSION_ID}}-autobump";
+            }} >> "$GITHUB_OUTPUT"
+        fi
 
         echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT"
         "#
     })
     .id("bump-version")
     .add_env(("OLD_VERSION", current_version.to_string()))
-    .add_env(("BUMP_TYPE", bump_type.to_string()));
+    .add_env(("BUMP_TYPE", bump_type.to_string()))
+    .add_env(("WORKING_DIR", "${{ inputs.working-directory }}"));
 
     let new_version = StepOutput::new(&step, "new_version");
-    (step, new_version)
+    let title = StepOutput::new(&step, "title");
+    let body = StepOutput::new(&step, "body");
+    let branch_name = StepOutput::new(&step, "branch_name");
+    (step, new_version, title, body, branch_name)
 }
 
-fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step<Use> {
-    let formatted_version = format!("v{new_version}");
-
+fn create_pull_request(
+    title: StepOutput,
+    body: StepOutput,
+    generated_token: StepOutput,
+    branch_name: StepOutput,
+) -> Step<Use> {
     named::uses("peter-evans", "create-pull-request", "v7").with(
         Input::default()
-            .add("title", format!("Bump version to {new_version}"))
-            .add(
-                "body",
-                format!("This PR bumps the version of this extension to {formatted_version}",),
-            )
-            .add(
-                "commit-message",
-                format!("Bump version to {formatted_version}"),
-            )
-            .add("branch", "zed-zippy-autobump")
+            .add("title", title.to_string())
+            .add("body", body.to_string())
+            .add("commit-message", title.to_string())
+            .add("branch", branch_name.to_string())
             .add(
                 "committer",
                 "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
@@ -328,6 +365,7 @@ fn trigger_release(
     let (get_extension_id, extension_id) = get_extension_id();
 
     let job = dependant_job(dependencies)
+        .defaults(extension_job_defaults())
         .with_repository_owner_guard()
         .runs_on(runners::LINUX_SMALL)
         .add_step(generate_token)

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

@@ -3,15 +3,13 @@ use indoc::indoc;
 
 use crate::tasks::workflows::{
     extension_bump::compare_versions,
-    run_tests::{
-        fetch_ts_query_ls, orchestrate_without_package_filter, run_ts_query_ls, tests_pass,
-    },
+    run_tests::{fetch_ts_query_ls, orchestrate_for_extension, run_ts_query_ls, tests_pass},
     runners,
     steps::{
-        self, CommonJobConditions, FluentBuilder, NamedJob, cache_rust_dependencies_namespace,
-        named,
+        self, BASH_SHELL, CommonJobConditions, FluentBuilder, NamedJob,
+        cache_rust_dependencies_namespace, named,
     },
-    vars::{PathCondition, StepOutput, one_workflow_per_non_main_branch},
+    vars::{PathCondition, StepOutput, WorkflowInput, one_workflow_per_non_main_branch},
 };
 
 pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "03d8e9aee95ea6117d75a48bcac2e19241f6e667";
@@ -25,8 +23,10 @@ pub(crate) fn extension_tests() -> Workflow {
     let should_check_extension =
         PathCondition::new("check_extension", r"^(extension\.toml|.*\.scm)$");
 
-    let orchestrate =
-        orchestrate_without_package_filter(&[&should_check_rust, &should_check_extension]);
+    let orchestrate = with_extension_defaults(orchestrate_for_extension(&[
+        &should_check_rust,
+        &should_check_extension,
+    ]));
 
     let jobs = [
         orchestrate,
@@ -34,10 +34,17 @@ pub(crate) fn extension_tests() -> Workflow {
         should_check_extension.guard(check_extension()),
     ];
 
-    let tests_pass = tests_pass(&jobs);
+    let tests_pass = with_extension_defaults(tests_pass(&jobs));
+
+    let working_directory = WorkflowInput::string("working-directory", Some(".".to_owned()));
 
     named::workflow()
-        .add_event(Event::default().workflow_call(WorkflowCall::default()))
+        .add_event(
+            Event::default().workflow_call(
+                WorkflowCall::default()
+                    .add_input(working_directory.name, working_directory.call_input()),
+            ),
+        )
         .concurrency(one_workflow_per_non_main_branch())
         .add_env(("CARGO_TERM_COLOR", "always"))
         .add_env(("RUST_BACKTRACE", 1))
@@ -58,27 +65,66 @@ fn install_rust_target() -> Step<Run> {
     named::bash(format!("rustup target add {EXTENSION_RUST_TARGET}",))
 }
 
-fn run_clippy() -> Step<Run> {
-    named::bash("cargo clippy --release --all-features -- --deny warnings")
+fn get_package_name() -> (Step<Run>, StepOutput) {
+    let step = named::bash(indoc! {r#"
+        PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' < Cargo.toml | head -1 | tr -d '[:space:]')"
+        echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
+    "#})
+    .id("get-package-name");
+
+    let output = StepOutput::new(&step, "package_name");
+    (step, output)
+}
+
+fn cargo_fmt_package(package_name: &StepOutput) -> Step<Run> {
+    named::bash(r#"cargo fmt -p "$PACKAGE_NAME" -- --check"#)
+        .add_env(("PACKAGE_NAME", package_name.to_string()))
+}
+
+fn run_clippy(package_name: &StepOutput) -> Step<Run> {
+    named::bash(r#"cargo clippy -p "$PACKAGE_NAME" --release --all-features -- --deny warnings"#)
+        .add_env(("PACKAGE_NAME", package_name.to_string()))
+}
+
+fn run_nextest(package_name: &StepOutput) -> Step<Run> {
+    named::bash(
+        r#"cargo nextest run -p "$PACKAGE_NAME" --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n 's|host: ||p')""#,
+    )
+    .add_env(("PACKAGE_NAME", package_name.to_string()))
+    .add_env(("NEXTEST_NO_TESTS", "warn"))
+}
+
+fn extension_job_defaults() -> Defaults {
+    Defaults::default().run(
+        RunDefaults::default()
+            .shell(BASH_SHELL)
+            .working_directory("${{ inputs.working-directory }}"),
+    )
+}
+
+fn with_extension_defaults(named_job: NamedJob) -> NamedJob {
+    NamedJob {
+        name: named_job.name,
+        job: named_job.job.defaults(extension_job_defaults()),
+    }
 }
 
 fn check_rust() -> NamedJob {
+    let (get_package, package_name) = get_package_name();
+
     let job = Job::default()
+        .defaults(extension_job_defaults())
         .with_repository_owner_guard()
         .runs_on(runners::LINUX_LARGE_RAM)
         .timeout_minutes(6u32)
         .add_step(steps::checkout_repo())
         .add_step(steps::cache_rust_dependencies_namespace())
         .add_step(install_rust_target())
-        .add_step(steps::cargo_fmt())
-        .add_step(run_clippy())
+        .add_step(get_package)
+        .add_step(cargo_fmt_package(&package_name))
+        .add_step(run_clippy(&package_name))
         .add_step(steps::cargo_install_nextest())
-        .add_step(
-            steps::cargo_nextest(runners::Platform::Linux)
-                // Set the target to the current platform again
-                .with_target("$(rustc -vV | sed -n 's|host: ||p')")
-                .add_env(("NEXTEST_NO_TESTS", "warn")),
-        );
+        .add_step(run_nextest(&package_name));
 
     named::job(job)
 }
@@ -88,6 +134,7 @@ pub(crate) fn check_extension() -> NamedJob {
     let (check_version_job, version_changed, _) = compare_versions();
 
     let job = Job::default()
+        .defaults(extension_job_defaults())
         .with_repository_owner_guard()
         .runs_on(runners::LINUX_LARGE_RAM)
         .timeout_minutes(6u32)
@@ -124,8 +171,8 @@ pub fn download_zed_extension_cli(cache_hit: StepOutput) -> Step<Run> {
     named::bash(
     indoc! {
         r#"
-        wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
-        chmod +x zed-extension
+        wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" -O "$GITHUB_WORKSPACE/zed-extension"
+        chmod +x "$GITHUB_WORKSPACE/zed-extension"
         "#,
     }
     ).if_condition(Expression::new(format!("{} != 'true'", cache_hit.expr())))
@@ -136,7 +183,7 @@ pub fn check() -> Step<Run> {
         r#"
         mkdir -p /tmp/ext-scratch
         mkdir -p /tmp/ext-output
-        ./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
+        "$GITHUB_WORKSPACE/zed-extension" --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
         "#
     })
 }

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

@@ -127,8 +127,9 @@ fn fetch_extension_repos(filter_repos_input: &WorkflowInput) -> (NamedJob, JobOu
         .id("calc-changes")
         .add_env(("PREV_COMMIT", prev_commit.to_string()));
 
-        let removed_ci = StepOutput::new(&step, "removed_ci");
-        let removed_shared = StepOutput::new(&step, "removed_shared");
+        // These are created in the for-loop above and thus do exist
+        let removed_ci = StepOutput::new_unchecked(&step, "removed_ci");
+        let removed_shared = StepOutput::new_unchecked(&step, "removed_shared");
 
         (step, removed_ci, removed_shared)
     }

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

@@ -97,14 +97,18 @@ pub(crate) fn run_tests() -> Workflow {
 // Generates a bash script that checks changed files against regex patterns
 // and sets GitHub output variables accordingly
 pub fn orchestrate(rules: &[&PathCondition]) -> NamedJob {
-    orchestrate_impl(rules, true)
+    orchestrate_impl(rules, true, false)
 }
 
-pub fn orchestrate_without_package_filter(rules: &[&PathCondition]) -> NamedJob {
-    orchestrate_impl(rules, false)
+pub fn orchestrate_for_extension(rules: &[&PathCondition]) -> NamedJob {
+    orchestrate_impl(rules, false, true)
 }
 
-fn orchestrate_impl(rules: &[&PathCondition], include_package_filter: bool) -> NamedJob {
+fn orchestrate_impl(
+    rules: &[&PathCondition],
+    include_package_filter: bool,
+    filter_by_working_directory: bool,
+) -> NamedJob {
     let name = "orchestrate".to_owned();
     let step_name = "filter".to_owned();
     let mut script = String::new();
@@ -121,6 +125,22 @@ fn orchestrate_impl(rules: &[&PathCondition], include_package_filter: bool) -> N
         fi
         CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" "$GITHUB_SHA")"
 
+    "#});
+
+    if filter_by_working_directory {
+        script.push_str(indoc::indoc! {r#"
+        # When running from a subdirectory, git diff returns repo-root-relative paths.
+        # Filter to only files within the current working directory and strip the prefix.
+        REPO_SUBDIR="$(git rev-parse --show-prefix)"
+        REPO_SUBDIR="${REPO_SUBDIR%/}"
+        if [ -n "$REPO_SUBDIR" ]; then
+            CHANGED_FILES="$(echo "$CHANGED_FILES" | grep "^${REPO_SUBDIR}/" | sed "s|^${REPO_SUBDIR}/||" || true)"
+        fi
+
+    "#});
+    }
+
+    script.push_str(indoc::indoc! {r#"
         check_pattern() {
           local output_name="$1"
           local pattern="$2"
@@ -298,8 +318,8 @@ pub(crate) fn fetch_ts_query_ls() -> Step<Use> {
 
 pub(crate) fn run_ts_query_ls() -> Step<Run> {
     named::bash(formatdoc!(
-        r#"tar -xf {TS_QUERY_LS_FILE}
-        ./ts_query_ls format --check . || {{
+        r#"tar -xf "$GITHUB_WORKSPACE/{TS_QUERY_LS_FILE}" -C "$GITHUB_WORKSPACE"
+        "$GITHUB_WORKSPACE/ts_query_ls" format --check . || {{
             echo "Found unformatted queries, please format them with ts_query_ls."
             echo "For easy use, install the Tree-sitter query extension:"
             echo "zed://extension/tree-sitter-query"

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

@@ -10,7 +10,7 @@ pub(crate) fn use_clang(job: Job) -> Job {
 
 const SCCACHE_R2_BUCKET: &str = "sccache-zed";
 
-const BASH_SHELL: &str = "bash -euxo pipefail {0}";
+pub(crate) const BASH_SHELL: &str = "bash -euxo pipefail {0}";
 // https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell
 pub const PWSH_SHELL: &str = "pwsh";
 
@@ -24,13 +24,6 @@ pub(crate) fn cargo_nextest(platform: Platform) -> Nextest {
 }
 
 impl Nextest {
-    pub(crate) fn with_target(mut self, target: &str) -> Step<Run> {
-        if let Some(nextest_command) = self.0.value.run.as_mut() {
-            nextest_command.push_str(&format!(r#" --target "{target}""#));
-        }
-        self.into()
-    }
-
     #[allow(dead_code)]
     pub(crate) fn with_filter_expr(mut self, filter_expr: &str) -> Self {
         if let Some(nextest_command) = self.0.value.run.as_mut() {

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

@@ -156,14 +156,31 @@ pub(crate) struct StepOutput {
 
 impl StepOutput {
     pub fn new<T>(step: &Step<T>, name: &'static str) -> Self {
-        Self {
-            name,
-            step_id: step
-                .value
-                .id
-                .clone()
-                .expect("Steps that produce outputs must have an ID"),
-        }
+        let step_id = step
+            .value
+            .id
+            .clone()
+            .expect("Steps that produce outputs must have an ID");
+
+        assert!(
+            step.value
+                .run
+                .as_ref()
+                .is_none_or(|run_command| run_command.contains(name)),
+            "Step Output name {name} must occur at least once in run command with ID {step_id}!"
+        );
+
+        Self { name, step_id }
+    }
+
+    pub fn new_unchecked<T>(step: &Step<T>, name: &'static str) -> Self {
+        let step_id = step
+            .value
+            .id
+            .clone()
+            .expect("Steps that produce outputs must have an ID");
+
+        Self { name, step_id }
     }
 
     pub fn expr(&self) -> String {