diff --git a/.github/workflows/extension_workflow_rollout.yml b/.github/workflows/extension_workflow_rollout.yml new file mode 100644 index 0000000000000000000000000000000000000000..80015f0601d733e7f38642793fa385630da73f65 --- /dev/null +++ b/.github/workflows/extension_workflow_rollout.yml @@ -0,0 +1,101 @@ +# Generated from xtask::workflows::extension_workflow_rollout +# Rebuild with `cargo xtask workflows`. +name: extension_workflow_rollout +env: + CARGO_TERM_COLOR: always +on: + workflow_dispatch: {} +jobs: + fetch_extension_repos: + runs-on: namespace-profile-2x4-ubuntu-2404 + steps: + - id: list-repos + name: extension_workflow_rollout::fetch_extension_repos::get_repositories + uses: actions/github-script@v7 + with: + script: | + const repos = await github.paginate(github.rest.repos.listForOrg, { + org: 'zed-extensions', + type: 'public', + per_page: 100, + }); + + const filteredRepos = repos + .filter(repo => !repo.archived) + .filter(repo => repo.name !== 'workflows' && repo.name !== 'material-icon-theme') + .map(repo => repo.name); + + console.log(`Found ${filteredRepos.length} extension repos`); + return filteredRepos; + result-encoding: json + outputs: + repos: ${{ steps.list-repos.outputs.result }} + timeout-minutes: 5 + rollout_workflows_to_extension: + needs: + - fetch_extension_repos + if: needs.fetch_extension_repos.outputs.repos != '[]' + runs-on: namespace-profile-2x4-ubuntu-2404 + strategy: + matrix: + repo: ${{ fromJson(needs.fetch_extension_repos.outputs.repos) }} + fail-fast: false + max-parallel: 5 + steps: + - id: get-app-token + name: steps::authenticate_as_zippy + uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 + with: + app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} + private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} + - name: checkout_zed_repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + clean: false + path: zed + - name: steps::checkout_repo_with_token + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + clean: false + token: ${{ steps.get-app-token.outputs.token }} + repository: zed-extensions/${{ matrix.repo }} + path: extension + - name: extension_workflow_rollout::rollout_workflows_to_extension::copy_workflow_files + run: | + mkdir -p extension/.github/workflows + cp zed/extensions/workflows/*.yml extension/.github/workflows/ + shell: bash -euxo pipefail {0} + - id: short-sha + name: extension_workflow_rollout::rollout_workflows_to_extension::get_short_sha + run: | + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + shell: bash -euxo pipefail {0} + working-directory: zed + - id: create-pr + name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request + uses: peter-evans/create-pull-request@v7 + with: + path: extension + title: Update CI workflows to zed@${{ steps.short-sha.outputs.sha_short }} + body: | + This PR updates the CI workflow files from the main Zed repository + based on the commit zed-industries/zed@${{ github.sha }} + commit-message: Update CI workflows to zed@${{ steps.short-sha.outputs.sha_short }} + branch: update-workflows + committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> + author: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com> + base: main + delete-branch: true + token: ${{ steps.get-app-token.outputs.token }} + sign-commits: true + - name: extension_workflow_rollout::rollout_workflows_to_extension::enable_auto_merge + run: | + PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}" + if [ -n "$PR_NUMBER" ]; then + cd extension + gh pr merge "$PR_NUMBER" --auto --squash + fi + shell: bash -euxo pipefail {0} + env: + GH_TOKEN: ${{ steps.get-app-token.outputs.token }} + timeout-minutes: 10 diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs index fe476355203a69c962081c36fe350460b9df6f6b..3f94527be9df2b74e5ca98c2ff2ce683f8b5bb30 100644 --- a/tooling/xtask/src/tasks/workflows.rs +++ b/tooling/xtask/src/tasks/workflows.rs @@ -12,6 +12,7 @@ mod danger; mod extension_bump; mod extension_release; mod extension_tests; +mod extension_workflow_rollout; mod extensions; mod nix_build; mod release_nightly; @@ -121,6 +122,7 @@ pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> { WorkflowFile::zed(extension_tests::extension_tests), WorkflowFile::zed(extension_bump::extension_bump), WorkflowFile::zed(extension_release::extension_release), + WorkflowFile::zed(extension_workflow_rollout::extension_workflow_rollout), /* workflows used for CI/CD in extension repositories */ WorkflowFile::extension(extensions::run_tests::run_tests), WorkflowFile::extension(extensions::bump_version::bump_version), diff --git a/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7926ed77898e5b06586fe6a4181b074b7955179 --- /dev/null +++ b/tooling/xtask/src/tasks/workflows/extension_workflow_rollout.rs @@ -0,0 +1,174 @@ +use gh_workflow::{Event, Expression, Job, Run, Step, Strategy, Use, Workflow, WorkflowDispatch}; +use indoc::indoc; +use serde_json::json; + +use crate::tasks::workflows::{ + runners, + steps::{self, NamedJob, named}, + vars::StepOutput, +}; + +const EXCLUDED_REPOS: &[&str] = &["workflows", "material-icon-theme"]; + +pub(crate) fn extension_workflow_rollout() -> Workflow { + let fetch_repos = fetch_extension_repos(); + let rollout_workflows = rollout_workflows_to_extension(&fetch_repos); + + named::workflow() + .on(Event::default().workflow_dispatch(WorkflowDispatch::default())) + .add_env(("CARGO_TERM_COLOR", "always")) + .add_job(fetch_repos.name, fetch_repos.job) + .add_job(rollout_workflows.name, rollout_workflows.job) +} + +fn fetch_extension_repos() -> NamedJob { + fn get_repositories() -> (Step, StepOutput) { + let exclusion_filter = EXCLUDED_REPOS + .iter() + .map(|repo| format!("repo.name !== '{}'", repo)) + .collect::>() + .join(" && "); + + let step = named::uses("actions", "github-script", "v7") + .id("list-repos") + .add_with(( + "script", + format!( + indoc! {r#" + const repos = await github.paginate(github.rest.repos.listForOrg, {{ + org: 'zed-extensions', + type: 'public', + per_page: 100, + }}); + + const filteredRepos = repos + .filter(repo => !repo.archived) + .filter(repo => {}) + .map(repo => repo.name); + + console.log(`Found ${{filteredRepos.length}} extension repos`); + return filteredRepos; + "#}, + exclusion_filter + ), + )) + .add_with(("result-encoding", "json")); + + let filtered_repos = StepOutput::new(&step, "result"); + + (step, filtered_repos) + } + + let (get_org_repositories, list_repos_output) = get_repositories(); + + let job = Job::default() + .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() -> Step { + steps::checkout_repo() + .name("checkout_zed_repo") + .add_with(("path", "zed")) + } + + fn checkout_extension_repo(token: &StepOutput) -> Step { + steps::checkout_repo_with_token(token) + .add_with(("repository", "zed-extensions/${{ matrix.repo }}")) + .add_with(("path", "extension")) + } + + fn copy_workflow_files() -> Step { + named::bash(indoc! {r#" + mkdir -p extension/.github/workflows + cp zed/extensions/workflows/*.yml extension/.github/workflows/ + "#}) + } + + fn get_short_sha() -> (Step, StepOutput) { + let step = named::bash(indoc! {r#" + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + "#}) + .id("short-sha") + .working_directory("zed"); + + let step_output = StepOutput::new(&step, "sha_short"); + + (step, step_output) + } + + fn create_pull_request(token: &StepOutput, short_sha: StepOutput) -> Step { + let title = format!("Update CI workflows to zed@{}", short_sha); + + named::uses("peter-evans", "create-pull-request", "v7") + .add_with(("path", "extension")) + .add_with(("title", title.clone())) + .add_with(( + "body", + 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(("commit-message", title)) + .add_with(("branch", "update-workflows")) + .add_with(( + "committer", + "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", + )) + .add_with(( + "author", + "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", + )) + .add_with(("base", "main")) + .add_with(("delete-branch", true)) + .add_with(("token", token.to_string())) + .add_with(("sign-commits", true)) + .id("create-pr") + } + + fn enable_auto_merge(token: &StepOutput) -> Step { + named::bash(indoc! {r#" + PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}" + if [ -n "$PR_NUMBER" ]; then + cd extension + gh pr merge "$PR_NUMBER" --auto --squash + fi + "#}) + .add_env(("GH_TOKEN", token.to_string())) + } + + let (authenticate, token) = steps::authenticate_as_zippy(); + let (calculate_short_sha, short_sha) = get_short_sha(); + + let job = Job::default() + .needs([fetch_repos_job.name.clone()]) + .cond(Expression::new(format!( + "needs.{}.outputs.repos != '[]'", + fetch_repos_job.name + ))) + .runs_on(runners::LINUX_SMALL) + .timeout_minutes(10u32) + .strategy( + Strategy::default() + .fail_fast(false) + .max_parallel(5u32) + .matrix(json!({ + "repo": format!("${{{{ fromJson(needs.{}.outputs.repos) }}}}", fetch_repos_job.name) + })), + ) + .add_step(authenticate) + .add_step(checkout_zed_repo()) + .add_step(checkout_extension_repo(&token)) + .add_step(copy_workflow_files()) + .add_step(calculate_short_sha) + .add_step(create_pull_request(&token, short_sha)) + .add_step(enable_auto_merge(&token)); + + named::job(job) +}