From e5fb57c8afc76731df15be3ae5510fb1d5bce965 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 11 Mar 2026 11:53:22 +0100 Subject: [PATCH] extension_rollout: Add incremental rollout (#51264) This will allow us to test changes against just a subset of extensions. Another advantage is that extension workflows will be pinned, which allows for easier debugging and better predictability. Release Notes: - N/A --- .github/workflows/bump_patch_version.yml | 2 +- .../workflows/extension_workflow_rollout.yml | 145 ++++++---- tooling/xtask/src/tasks/workflows.rs | 83 +++++- .../workflows/extension_workflow_rollout.rs | 263 ++++++++++++------ .../workflows/extensions/bump_version.rs | 8 +- .../tasks/workflows/extensions/run_tests.rs | 9 +- tooling/xtask/src/tasks/workflows/steps.rs | 20 +- 7 files changed, 356 insertions(+), 174 deletions(-) diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml index 480d8b0ada98e859d2e72b49a39805ffe8f72b25..62540321ed755f2fd3879a7ddfc3a37237d8e7de 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -23,8 +23,8 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: clean: false - token: ${{ steps.get-app-token.outputs.token }} ref: ${{ inputs.branch }} + token: ${{ steps.get-app-token.outputs.token }} - name: bump_patch_version::run_bump_patch_version::bump_patch_version run: | channel="$(cat crates/zed/RELEASE_CHANNEL)" diff --git a/.github/workflows/extension_workflow_rollout.yml b/.github/workflows/extension_workflow_rollout.yml index 9bfac06d4527985553ba3d04e64c656ee5bf85e4..cbb813d91749bf3843b64372f12e50f6a3c3e785 100644 --- a/.github/workflows/extension_workflow_rollout.yml +++ b/.github/workflows/extension_workflow_rollout.yml @@ -4,12 +4,57 @@ name: extension_workflow_rollout env: CARGO_TERM_COLOR: always on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + filter-repos: + description: Comma-separated list of repository names to rollout to. Leave empty for all repos. + type: string + default: '' + change-description: + description: Description for the changes to be expected with this rollout + type: string + default: '' jobs: fetch_extension_repos: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.ref == 'refs/heads/main' runs-on: namespace-profile-2x4-ubuntu-2404 steps: + - name: checkout_zed_repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + clean: false + fetch-depth: 0 + - id: prev-tag + name: extension_workflow_rollout::fetch_extension_repos::get_previous_tag_commit + run: | + PREV_COMMIT=$(git rev-parse "extension-workflows^{commit}" 2>/dev/null || echo "") + if [ -z "$PREV_COMMIT" ]; then + echo "::error::No previous rollout tag 'extension-workflows' found. Cannot determine file changes." + exit 1 + fi + echo "Found previous rollout at commit: $PREV_COMMIT" + echo "prev_commit=$PREV_COMMIT" >> "$GITHUB_OUTPUT" + - id: calc-changes + name: extension_workflow_rollout::fetch_extension_repos::get_removed_files + run: | + for workflow_type in "ci" "shared"; do + if [ "$workflow_type" = "ci" ]; then + WORKFLOW_DIR="extensions/workflows" + else + WORKFLOW_DIR="extensions/workflows/shared" + fi + + REMOVED=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \ + awk '/^D/ { print $2 } /^R/ { print $2 }' | \ + xargs -I{} basename {} 2>/dev/null | \ + tr '\n' ' ' || echo "") + REMOVED=$(echo "$REMOVED" | xargs) + + echo "Removed files for $workflow_type: $REMOVED" + echo "removed_${workflow_type}=$REMOVED" >> "$GITHUB_OUTPUT" + done + env: + PREV_COMMIT: ${{ steps.prev-tag.outputs.prev_commit }} - id: list-repos name: extension_workflow_rollout::fetch_extension_repos::get_repositories uses: actions/github-script@v7 @@ -21,16 +66,42 @@ jobs: per_page: 100, }); - const filteredRepos = repos + let filteredRepos = repos .filter(repo => !repo.archived) .map(repo => repo.name); + const filterInput = `${{ inputs.filter-repos }}`.trim(); + if (filterInput.length > 0) { + const allowedNames = filterInput.split(',').map(s => s.trim()).filter(s => s.length > 0); + filteredRepos = filteredRepos.filter(name => allowedNames.includes(name)); + console.log(`Filter applied. Matched ${filteredRepos.length} repos from ${allowedNames.length} requested.`); + } + console.log(`Found ${filteredRepos.length} extension repos`); return filteredRepos; result-encoding: json + - name: steps::cache_rust_dependencies_namespace + uses: namespacelabs/nscloud-cache-action@v1 + with: + cache: rust + path: ~/.rustup + - name: extension_workflow_rollout::fetch_extension_repos::generate_workflow_files + run: | + cargo xtask workflows "$COMMIT_SHA" + env: + COMMIT_SHA: ${{ github.sha }} + - name: extension_workflow_rollout::fetch_extension_repos::upload_workflow_files + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: extension-workflow-files + path: extensions/workflows/**/*.yml + if-no-files-found: error outputs: repos: ${{ steps.list-repos.outputs.result }} - timeout-minutes: 5 + prev_commit: ${{ steps.prev-tag.outputs.prev_commit }} + removed_ci: ${{ steps.calc-changes.outputs.removed_ci }} + removed_shared: ${{ steps.calc-changes.outputs.removed_shared }} + timeout-minutes: 10 rollout_workflows_to_extension: needs: - fetch_extension_repos @@ -53,59 +124,28 @@ jobs: permission-pull-requests: write permission-contents: write permission-workflows: write - - name: checkout_zed_repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - with: - clean: false - fetch-depth: 0 - path: zed - name: checkout_extension_repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: clean: false - token: ${{ steps.generate-token.outputs.token }} path: extension repository: zed-extensions/${{ matrix.repo }} - - id: prev-tag - name: extension_workflow_rollout::rollout_workflows_to_extension::get_previous_tag_commit - run: | - PREV_COMMIT=$(git rev-parse "extension-workflows^{commit}" 2>/dev/null || echo "") - if [ -z "$PREV_COMMIT" ]; then - echo "::error::No previous rollout tag 'extension-workflows' found. Cannot determine file changes." - exit 1 - fi - echo "Found previous rollout at commit: $PREV_COMMIT" - echo "prev_commit=$PREV_COMMIT" >> "$GITHUB_OUTPUT" - working-directory: zed - - id: calc-changes - name: extension_workflow_rollout::rollout_workflows_to_extension::get_removed_files + token: ${{ steps.generate-token.outputs.token }} + - name: extension_workflow_rollout::rollout_workflows_to_extension::download_workflow_files + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 + with: + name: extension-workflow-files + path: workflow-files + - name: extension_workflow_rollout::rollout_workflows_to_extension::sync_workflow_files run: | + mkdir -p extension/.github/workflows + if [ "$MATRIX_REPO" = "workflows" ]; then - WORKFLOW_DIR="extensions/workflows" + REMOVED_FILES="$REMOVED_CI" else - WORKFLOW_DIR="extensions/workflows/shared" + REMOVED_FILES="$REMOVED_SHARED" fi - echo "Calculating changes from $PREV_COMMIT to HEAD for $WORKFLOW_DIR" - - # Get deleted files (status D) and renamed files (status R - old name needs removal) - # Using -M to detect renames, then extracting files that are gone from their original location - REMOVED_FILES=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \ - awk '/^D/ { print $2 } /^R/ { print $2 }' | \ - xargs -I{} basename {} 2>/dev/null | \ - tr '\n' ' ' || echo "") - - REMOVED_FILES=$(echo "$REMOVED_FILES" | xargs) - - echo "Files to remove: $REMOVED_FILES" - echo "removed_files=$REMOVED_FILES" >> "$GITHUB_OUTPUT" - env: - PREV_COMMIT: ${{ steps.prev-tag.outputs.prev_commit }} - MATRIX_REPO: ${{ matrix.repo }} - working-directory: zed - - name: extension_workflow_rollout::rollout_workflows_to_extension::sync_workflow_files - run: | - mkdir -p extension/.github/workflows cd extension/.github/workflows if [ -n "$REMOVED_FILES" ]; then @@ -119,18 +159,18 @@ jobs: cd - > /dev/null if [ "$MATRIX_REPO" = "workflows" ]; then - cp zed/extensions/workflows/*.yml extension/.github/workflows/ + cp workflow-files/extensions/workflows/*.yml extension/.github/workflows/ else - cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/ + cp workflow-files/extensions/workflows/shared/*.yml extension/.github/workflows/ fi env: - REMOVED_FILES: ${{ steps.calc-changes.outputs.removed_files }} + REMOVED_CI: ${{ needs.fetch_extension_repos.outputs.removed_ci }} + REMOVED_SHARED: ${{ needs.fetch_extension_repos.outputs.removed_shared }} MATRIX_REPO: ${{ matrix.repo }} - id: short-sha name: extension_workflow_rollout::rollout_workflows_to_extension::get_short_sha run: | - echo "sha_short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" - working-directory: zed + echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT" - id: create-pr name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request uses: peter-evans/create-pull-request@v7 @@ -140,6 +180,8 @@ jobs: body: | This PR updates the CI workflow files from the main Zed repository based on the commit zed-industries/zed@${{ github.sha }} + + ${{ inputs.change-description }} commit-message: Update CI workflows to `${{ steps.short-sha.outputs.sha_short }}` branch: update-workflows committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> @@ -151,16 +193,17 @@ jobs: - name: extension_workflow_rollout::rollout_workflows_to_extension::enable_auto_merge run: | if [ -n "$PR_NUMBER" ]; then - cd extension gh pr merge "$PR_NUMBER" --auto --squash fi env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} PR_NUMBER: ${{ steps.create-pr.outputs.pull-request-number }} + working-directory: extension timeout-minutes: 10 create_rollout_tag: needs: - rollout_workflows_to_extension + if: inputs.filter-repos == '' runs-on: namespace-profile-2x4-ubuntu-2404 steps: - id: generate-token diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs index 9151b9c671ef42e3dc54661f80438a4e31aff1e9..26596c9401c1d3c500a8c1cb18083d525c934e20 100644 --- a/tooling/xtask/src/tasks/workflows.rs +++ b/tooling/xtask/src/tasks/workflows.rs @@ -29,38 +29,99 @@ mod runners; mod steps; mod vars; +#[derive(Clone)] +pub(crate) struct GitSha(String); + +impl AsRef for GitSha { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[allow( + clippy::disallowed_methods, + reason = "This runs only in a CLI environment" +)] +fn parse_ref(value: &str) -> Result { + const GIT_SHA_LENGTH: usize = 40; + (value.len() == GIT_SHA_LENGTH) + .then_some(value) + .ok_or_else(|| { + format!( + "Git SHA has wrong length! \ + Only SHAs with a full length of {GIT_SHA_LENGTH} are supported, found {len} characters.", + len = value.len() + ) + }) + .and_then(|value| { + let mut tmp = [0; 4]; + value + .chars() + .all(|char| u16::from_str_radix(char.encode_utf8(&mut tmp), 16).is_ok()).then_some(value) + .ok_or_else(|| "Not a valid Git SHA".to_owned()) + }) + .and_then(|sha| { + std::process::Command::new("git") + .args([ + "rev-parse", + "--quiet", + "--verify", + &format!("{sha}^{{commit}}") + ]) + .output() + .map_err(|_| "Failed to spawn Git command to verify SHA".to_owned()) + .and_then(|output| + output + .status.success() + .then_some(sha) + .ok_or_else(|| format!("SHA {sha} is not a valid Git SHA within this repository!"))) + }).map(|sha| GitSha(sha.to_owned())) +} + #[derive(Parser)] -pub struct GenerateWorkflowArgs {} +pub(crate) struct GenerateWorkflowArgs { + #[arg(value_parser = parse_ref)] + /// The Git SHA to use when invoking this + pub(crate) sha: Option, +} + +enum WorkflowSource { + Contextless(fn() -> Workflow), + WithContext(fn(&GenerateWorkflowArgs) -> Workflow), +} struct WorkflowFile { - source: fn() -> Workflow, + source: WorkflowSource, r#type: WorkflowType, } impl WorkflowFile { fn zed(f: fn() -> Workflow) -> WorkflowFile { WorkflowFile { - source: f, + source: WorkflowSource::Contextless(f), r#type: WorkflowType::Zed, } } - fn extension(f: fn() -> Workflow) -> WorkflowFile { + fn extension(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile { WorkflowFile { - source: f, + source: WorkflowSource::WithContext(f), r#type: WorkflowType::ExtensionCi, } } - fn extension_shared(f: fn() -> Workflow) -> WorkflowFile { + fn extension_shared(f: fn(&GenerateWorkflowArgs) -> Workflow) -> WorkflowFile { WorkflowFile { - source: f, + source: WorkflowSource::WithContext(f), r#type: WorkflowType::ExtensionsShared, } } - fn generate_file(&self) -> Result<()> { - let workflow = (self.source)(); + fn generate_file(&self, workflow_args: &GenerateWorkflowArgs) -> Result<()> { + let workflow = match &self.source { + WorkflowSource::Contextless(f) => f(), + WorkflowSource::WithContext(f) => f(workflow_args), + }; let workflow_folder = self.r#type.folder_path(); fs::create_dir_all(&workflow_folder).with_context(|| { @@ -124,7 +185,7 @@ impl WorkflowType { } } -pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> { +pub fn run_workflows(args: GenerateWorkflowArgs) -> Result<()> { if !Path::new("crates/zed/").is_dir() { anyhow::bail!("xtask workflows must be ran from the project root"); } @@ -154,7 +215,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> { ]; for workflow_file in workflows { - workflow_file.generate_file()?; + workflow_file.generate_file(&args)?; } workflow_checks::validate(Default::default()) diff --git a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs index 6f03ad1521850fb24c5bad7265ebf913228c5077..91154c91061fd2e8a51e60704eca0f9b0b94c900 100644 --- a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs +++ b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs @@ -6,46 +6,72 @@ use indoc::indoc; use serde_json::json; use crate::tasks::workflows::steps::CheckoutStep; +use crate::tasks::workflows::steps::cache_rust_dependencies_namespace; +use crate::tasks::workflows::vars::JobOutput; use crate::tasks::workflows::{ extension_bump::{RepositoryTarget, generate_token}, runners, steps::{self, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob, named}, - vars::{self, StepOutput}, + vars::{self, StepOutput, WorkflowInput}, }; const ROLLOUT_TAG_NAME: &str = "extension-workflows"; +const WORKFLOW_ARTIFACT_NAME: &str = "extension-workflow-files"; pub(crate) fn extension_workflow_rollout() -> Workflow { - let fetch_repos = fetch_extension_repos(); - let rollout_workflows = rollout_workflows_to_extension(&fetch_repos); - let create_tag = create_rollout_tag(&rollout_workflows); + let filter_repos_input = WorkflowInput::string("filter-repos", Some(String::new())) + .description( + "Comma-separated list of repository names to rollout to. Leave empty for all repos.", + ); + let extra_context_input = WorkflowInput::string("change-description", Some(String::new())) + .description("Description for the changes to be expected with this rollout"); + + let (fetch_repos, removed_ci, removed_shared) = fetch_extension_repos(&filter_repos_input); + let rollout_workflows = rollout_workflows_to_extension( + &fetch_repos, + removed_ci, + removed_shared, + &extra_context_input, + ); + let create_tag = create_rollout_tag(&rollout_workflows, &filter_repos_input); named::workflow() - .on(Event::default().workflow_dispatch(WorkflowDispatch::default())) + .on(Event::default().workflow_dispatch( + WorkflowDispatch::default() + .add_input(filter_repos_input.name, filter_repos_input.input()) + .add_input(extra_context_input.name, extra_context_input.input()), + )) .add_env(("CARGO_TERM_COLOR", "always")) .add_job(fetch_repos.name, fetch_repos.job) .add_job(rollout_workflows.name, rollout_workflows.job) .add_job(create_tag.name, create_tag.job) } -fn fetch_extension_repos() -> NamedJob { - fn get_repositories() -> (Step, StepOutput) { +fn fetch_extension_repos(filter_repos_input: &WorkflowInput) -> (NamedJob, JobOutput, JobOutput) { + fn get_repositories(filter_repos_input: &WorkflowInput) -> (Step, StepOutput) { let step = named::uses("actions", "github-script", "v7") .id("list-repos") .add_with(( "script", - indoc::indoc! {r#" - const repos = await github.paginate(github.rest.repos.listForOrg, { + formatdoc! {r#" + const repos = await github.paginate(github.rest.repos.listForOrg, {{ org: 'zed-extensions', type: 'public', per_page: 100, - }); + }}); - const filteredRepos = repos + let filteredRepos = repos .filter(repo => !repo.archived) .map(repo => repo.name); - console.log(`Found ${filteredRepos.length} extension repos`); + const filterInput = `{filter_repos_input}`.trim(); + if (filterInput.length > 0) {{ + const allowedNames = filterInput.split(',').map(s => s.trim()).filter(s => s.length > 0); + filteredRepos = filteredRepos.filter(name => allowedNames.includes(name)); + console.log(`Filter applied. Matched ${{filteredRepos.length}} repos from ${{allowedNames.length}} requested.`); + }} + + console.log(`Found ${{filteredRepos.length}} extension repos`); return filteredRepos; "#}, )) @@ -56,36 +82,12 @@ fn fetch_extension_repos() -> NamedJob { (step, filtered_repos) } - let (get_org_repositories, list_repos_output) = get_repositories(); - - let job = Job::default() - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.ref == 'refs/heads/main'" - ))) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(5u32) - .outputs([("repos".to_owned(), list_repos_output.to_string())]) - .add_step(get_org_repositories); - - named::job(job) -} - -fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { fn checkout_zed_repo() -> CheckoutStep { steps::checkout_repo() .with_full_history() - .with_path("zed") .with_custom_name("checkout_zed_repo") } - fn checkout_extension_repo(token: &StepOutput) -> CheckoutStep { - steps::checkout_repo() - .with_custom_name("checkout_extension_repo") - .with_token(token) - .with_repository("zed-extensions/${{ matrix.repo }}") - .with_path("extension") - } - fn get_previous_tag_commit() -> (Step, StepOutput) { let step = named::bash(formatdoc! {r#" PREV_COMMIT=$(git rev-parse "{ROLLOUT_TAG_NAME}^{{commit}}" 2>/dev/null || echo "") @@ -96,49 +98,126 @@ fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { echo "Found previous rollout at commit: $PREV_COMMIT" echo "prev_commit=$PREV_COMMIT" >> "$GITHUB_OUTPUT" "#}) - .id("prev-tag") - .working_directory("zed"); + .id("prev-tag"); let step_output = StepOutput::new(&step, "prev_commit"); (step, step_output) } - fn get_removed_files(prev_commit: &StepOutput) -> (Step, StepOutput) { - let step = named::bash(indoc::indoc! {r#" - if [ "$MATRIX_REPO" = "workflows" ]; then - WORKFLOW_DIR="extensions/workflows" - else - WORKFLOW_DIR="extensions/workflows/shared" - fi - - echo "Calculating changes from $PREV_COMMIT to HEAD for $WORKFLOW_DIR" + fn get_removed_files(prev_commit: &StepOutput) -> (Step, StepOutput, StepOutput) { + let step = named::bash(indoc! {r#" + for workflow_type in "ci" "shared"; do + if [ "$workflow_type" = "ci" ]; then + WORKFLOW_DIR="extensions/workflows" + else + WORKFLOW_DIR="extensions/workflows/shared" + fi + + REMOVED=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \ + awk '/^D/ { print $2 } /^R/ { print $2 }' | \ + xargs -I{} basename {} 2>/dev/null | \ + tr '\n' ' ' || echo "") + REMOVED=$(echo "$REMOVED" | xargs) + + echo "Removed files for $workflow_type: $REMOVED" + echo "removed_${workflow_type}=$REMOVED" >> "$GITHUB_OUTPUT" + done + "#}) + .id("calc-changes") + .add_env(("PREV_COMMIT", prev_commit.to_string())); - # Get deleted files (status D) and renamed files (status R - old name needs removal) - # Using -M to detect renames, then extracting files that are gone from their original location - REMOVED_FILES=$(git diff --name-status -M "$PREV_COMMIT" HEAD -- "$WORKFLOW_DIR" | \ - awk '/^D/ { print $2 } /^R/ { print $2 }' | \ - xargs -I{} basename {} 2>/dev/null | \ - tr '\n' ' ' || echo "") + let removed_ci = StepOutput::new(&step, "removed_ci"); + let removed_shared = StepOutput::new(&step, "removed_shared"); - REMOVED_FILES=$(echo "$REMOVED_FILES" | xargs) + (step, removed_ci, removed_shared) + } - echo "Files to remove: $REMOVED_FILES" - echo "removed_files=$REMOVED_FILES" >> "$GITHUB_OUTPUT" + fn generate_workflow_files() -> Step { + named::bash(indoc! {r#" + cargo xtask workflows "$COMMIT_SHA" "#}) - .id("calc-changes") - .working_directory("zed") - .add_env(("PREV_COMMIT", prev_commit.to_string())) - .add_env(("MATRIX_REPO", "${{ matrix.repo }}")); + .add_env(("COMMIT_SHA", "${{ github.sha }}")) + } - let removed_files = StepOutput::new(&step, "removed_files"); + fn upload_workflow_files() -> Step { + named::uses( + "actions", + "upload-artifact", + "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 + ) + .add_with(("name", WORKFLOW_ARTIFACT_NAME)) + .add_with(("path", "extensions/workflows/**/*.yml")) + .add_with(("if-no-files-found", "error")) + } - (step, removed_files) + let (get_org_repositories, list_repos_output) = get_repositories(filter_repos_input); + let (get_prev_tag, prev_commit) = get_previous_tag_commit(); + let (calc_changes, removed_ci, removed_shared) = get_removed_files(&prev_commit); + + let job = Job::default() + .cond(Expression::new(format!( + "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.ref == 'refs/heads/main'" + ))) + .runs_on(runners::LINUX_SMALL) + .timeout_minutes(10u32) + .outputs([ + ("repos".to_owned(), list_repos_output.to_string()), + ("prev_commit".to_owned(), prev_commit.to_string()), + ("removed_ci".to_owned(), removed_ci.to_string()), + ("removed_shared".to_owned(), removed_shared.to_string()), + ]) + .add_step(checkout_zed_repo()) + .add_step(get_prev_tag) + .add_step(calc_changes) + .add_step(get_org_repositories) + .add_step(cache_rust_dependencies_namespace()) + .add_step(generate_workflow_files()) + .add_step(upload_workflow_files()); + + let job = named::job(job); + let (removed_ci, removed_shared) = ( + removed_ci.as_job_output(&job), + removed_shared.as_job_output(&job), + ); + + (job, removed_ci, removed_shared) +} + +fn rollout_workflows_to_extension( + fetch_repos_job: &NamedJob, + removed_ci: JobOutput, + removed_shared: JobOutput, + extra_context_input: &WorkflowInput, +) -> NamedJob { + fn checkout_extension_repo(token: &StepOutput) -> CheckoutStep { + steps::checkout_repo() + .with_custom_name("checkout_extension_repo") + .with_token(token) + .with_repository("zed-extensions/${{ matrix.repo }}") + .with_path("extension") + } + + fn download_workflow_files() -> Step { + named::uses( + "actions", + "download-artifact", + "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 + ) + .add_with(("name", WORKFLOW_ARTIFACT_NAME)) + .add_with(("path", "workflow-files")) } - fn sync_workflow_files(removed_files: &StepOutput) -> Step { - named::bash(indoc::indoc! {r#" + fn sync_workflow_files(removed_ci: JobOutput, removed_shared: JobOutput) -> Step { + named::bash(indoc! {r#" mkdir -p extension/.github/workflows + + if [ "$MATRIX_REPO" = "workflows" ]; then + REMOVED_FILES="$REMOVED_CI" + else + REMOVED_FILES="$REMOVED_SHARED" + fi + cd extension/.github/workflows if [ -n "$REMOVED_FILES" ]; then @@ -152,40 +231,46 @@ fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { cd - > /dev/null if [ "$MATRIX_REPO" = "workflows" ]; then - cp zed/extensions/workflows/*.yml extension/.github/workflows/ + cp workflow-files/extensions/workflows/*.yml extension/.github/workflows/ else - cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/ + cp workflow-files/extensions/workflows/shared/*.yml extension/.github/workflows/ fi "#}) - .add_env(("REMOVED_FILES", removed_files.to_string())) + .add_env(("REMOVED_CI", removed_ci)) + .add_env(("REMOVED_SHARED", removed_shared)) .add_env(("MATRIX_REPO", "${{ matrix.repo }}")) } fn get_short_sha() -> (Step, StepOutput) { - let step = named::bash(indoc::indoc! {r#" - echo "sha_short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + let step = named::bash(indoc! {r#" + echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT" "#}) - .id("short-sha") - .working_directory("zed"); + .id("short-sha"); let step_output = StepOutput::new(&step, "sha_short"); (step, step_output) } - fn create_pull_request(token: &StepOutput, short_sha: &StepOutput) -> Step { + fn create_pull_request( + token: &StepOutput, + short_sha: &StepOutput, + context_input: &WorkflowInput, + ) -> Step { let title = format!("Update CI workflows to `{short_sha}`"); + let body = formatdoc! {r#" + This PR updates the CI workflow files from the main Zed repository + based on the commit zed-industries/zed@${{{{ github.sha }}}} + + {context_input} + "#, + }; + named::uses("peter-evans", "create-pull-request", "v7") .add_with(("path", "extension")) .add_with(("title", title.clone())) - .add_with(( - "body", - indoc::indoc! {r#" - This PR updates the CI workflow files from the main Zed repository - based on the commit zed-industries/zed@${{ github.sha }} - "#}, - )) + .add_with(("body", body)) .add_with(("commit-message", title)) .add_with(("branch", "update-workflows")) .add_with(( @@ -204,12 +289,12 @@ fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { } fn enable_auto_merge(token: &StepOutput) -> Step { - named::bash(indoc::indoc! {r#" + named::bash(indoc! {r#" if [ -n "$PR_NUMBER" ]; then - cd extension gh pr merge "$PR_NUMBER" --auto --squash fi "#}) + .working_directory("extension") .add_env(("GH_TOKEN", token.to_string())) .add_env(( "PR_NUMBER", @@ -228,8 +313,6 @@ fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { ]), ), ); - let (get_prev_tag, prev_commit) = get_previous_tag_commit(); - let (calc_changes, removed_files) = get_removed_files(&prev_commit); let (calculate_short_sha, short_sha) = get_short_sha(); let job = Job::default() @@ -249,19 +332,17 @@ fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob { })), ) .add_step(authenticate) - .add_step(checkout_zed_repo()) .add_step(checkout_extension_repo(&token)) - .add_step(get_prev_tag) - .add_step(calc_changes) - .add_step(sync_workflow_files(&removed_files)) + .add_step(download_workflow_files()) + .add_step(sync_workflow_files(removed_ci, removed_shared)) .add_step(calculate_short_sha) - .add_step(create_pull_request(&token, &short_sha)) + .add_step(create_pull_request(&token, &short_sha, extra_context_input)) .add_step(enable_auto_merge(&token)); named::job(job) } -fn create_rollout_tag(rollout_job: &NamedJob) -> NamedJob { +fn create_rollout_tag(rollout_job: &NamedJob, filter_repos_input: &WorkflowInput) -> NamedJob { fn checkout_zed_repo(token: &StepOutput) -> CheckoutStep { steps::checkout_repo().with_full_history().with_token(token) } @@ -297,6 +378,10 @@ fn create_rollout_tag(rollout_job: &NamedJob) -> NamedJob { let job = Job::default() .needs([rollout_job.name.clone()]) + .cond(Expression::new(format!( + "{filter_repos} == ''", + filter_repos = filter_repos_input.expr(), + ))) .runs_on(runners::LINUX_SMALL) .timeout_minutes(1u32) .add_step(authenticate) diff --git a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs index 2d82f1351f21645a77b1d13e158bd4142dbec069..4dc2560e2bea489566fb8eb5ad5d04701835de29 100644 --- a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs +++ b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs @@ -5,17 +5,18 @@ use gh_workflow::{ use indoc::indoc; use crate::tasks::workflows::{ + GenerateWorkflowArgs, GitSha, extensions::WithAppSecrets, runners, steps::{CommonJobConditions, NamedJob, named}, vars::{JobOutput, StepOutput, one_workflow_per_non_main_branch_and_token}, }; -pub(crate) fn bump_version() -> Workflow { +pub(crate) fn bump_version(args: &GenerateWorkflowArgs) -> 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); + let call_bump_version = call_bump_version(args.sha.as_ref(), &determine_bump_type, bump_type); named::workflow() .on(Event::default() @@ -32,6 +33,7 @@ pub(crate) fn bump_version() -> Workflow { } pub(crate) fn call_bump_version( + target_ref: Option<&GitSha>, depending_job: &NamedJob, bump_type: JobOutput, ) -> NamedJob { @@ -51,7 +53,7 @@ pub(crate) fn call_bump_version( "zed-industries", "zed", ".github/workflows/extension_bump.yml", - "main", + target_ref.map_or("main", AsRef::as_ref), ) .add_need(depending_job.name.clone()) .with( diff --git a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs index 0c0ca696612fa57903f35c0ea69404f5dc7d1fe0..ae8000c15cad3a206b9c02f8bc389a369f4df096 100644 --- a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs @@ -1,12 +1,13 @@ use gh_workflow::{Event, Job, Level, Permissions, PullRequest, Push, UsesJob, Workflow}; use crate::tasks::workflows::{ + GenerateWorkflowArgs, GitSha, steps::{NamedJob, named}, vars::one_workflow_per_non_main_branch_and_token, }; -pub(crate) fn run_tests() -> Workflow { - let call_extension_tests = call_extension_tests(); +pub(crate) fn run_tests(args: &GenerateWorkflowArgs) -> Workflow { + let call_extension_tests = call_extension_tests(args.sha.as_ref()); named::workflow() .on(Event::default() .pull_request(PullRequest::default().add_branch("**")) @@ -15,14 +16,14 @@ pub(crate) fn run_tests() -> Workflow { .add_job(call_extension_tests.name, call_extension_tests.job) } -pub(crate) fn call_extension_tests() -> NamedJob { +pub(crate) fn call_extension_tests(target_ref: Option<&GitSha>) -> NamedJob { let job = Job::default() .permissions(Permissions::default().contents(Level::Read)) .uses( "zed-industries", "zed", ".github/workflows/extension_tests.yml", - "main", + target_ref.map_or("main", AsRef::as_ref), ); named::job(job) diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs index 4d17be81322277d0093de5d547bf4f0849e38dc3..6bede217b74a1172db712b92ed3d50cd2af603b2 100644 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ b/tooling/xtask/src/tasks/workflows/steps.rs @@ -131,22 +131,12 @@ impl From for Step { FetchDepth::Full => step.add_with(("fetch-depth", 0)), FetchDepth::Custom(depth) => step.add_with(("fetch-depth", depth)), }) - .map(|step| match value.token { - Some(token) => step.add_with(("token", token)), - None => step, - }) - .map(|step| match value.path { - Some(path) => step.add_with(("path", path)), - None => step, - }) - .map(|step| match value.repository { - Some(repository) => step.add_with(("repository", repository)), - None => step, - }) - .map(|step| match value.ref_ { - Some(ref_) => step.add_with(("ref", ref_)), - None => step, + .when_some(value.path, |step, path| step.add_with(("path", path))) + .when_some(value.repository, |step, repository| { + step.add_with(("repository", repository)) }) + .when_some(value.ref_, |step, ref_| step.add_with(("ref", ref_))) + .when_some(value.token, |step, token| step.add_with(("token", token))) } }