@@ -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
@@ -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),
@@ -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<Use>, StepOutput) {
+ let exclusion_filter = EXCLUDED_REPOS
+ .iter()
+ .map(|repo| format!("repo.name !== '{}'", repo))
+ .collect::<Vec<_>>()
+ .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<Use> {
+ steps::checkout_repo()
+ .name("checkout_zed_repo")
+ .add_with(("path", "zed"))
+ }
+
+ fn checkout_extension_repo(token: &StepOutput) -> Step<Use> {
+ steps::checkout_repo_with_token(token)
+ .add_with(("repository", "zed-extensions/${{ matrix.repo }}"))
+ .add_with(("path", "extension"))
+ }
+
+ fn copy_workflow_files() -> Step<Run> {
+ named::bash(indoc! {r#"
+ mkdir -p extension/.github/workflows
+ cp zed/extensions/workflows/*.yml extension/.github/workflows/
+ "#})
+ }
+
+ fn get_short_sha() -> (Step<Run>, 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<Use> {
+ 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<gh_workflow::Run> {
+ 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)
+}