extension_workflow_rollout.rs

  1use gh_workflow::{Event, Expression, Job, Run, Step, Strategy, Use, Workflow, WorkflowDispatch};
  2use indoc::indoc;
  3use serde_json::json;
  4
  5use crate::tasks::workflows::{
  6    runners,
  7    steps::{self, NamedJob, named},
  8    vars::StepOutput,
  9};
 10
 11const EXCLUDED_REPOS: &[&str] = &["workflows", "material-icon-theme"];
 12
 13pub(crate) fn extension_workflow_rollout() -> Workflow {
 14    let fetch_repos = fetch_extension_repos();
 15    let rollout_workflows = rollout_workflows_to_extension(&fetch_repos);
 16
 17    named::workflow()
 18        .on(Event::default().workflow_dispatch(WorkflowDispatch::default()))
 19        .add_env(("CARGO_TERM_COLOR", "always"))
 20        .add_job(fetch_repos.name, fetch_repos.job)
 21        .add_job(rollout_workflows.name, rollout_workflows.job)
 22}
 23
 24fn fetch_extension_repos() -> NamedJob {
 25    fn get_repositories() -> (Step<Use>, StepOutput) {
 26        let exclusion_filter = EXCLUDED_REPOS
 27            .iter()
 28            .map(|repo| format!("repo.name !== '{}'", repo))
 29            .collect::<Vec<_>>()
 30            .join(" && ");
 31
 32        let step = named::uses("actions", "github-script", "v7")
 33            .id("list-repos")
 34            .add_with((
 35                "script",
 36                format!(
 37                    indoc! {r#"
 38                        const repos = await github.paginate(github.rest.repos.listForOrg, {{
 39                            org: 'zed-extensions',
 40                            type: 'public',
 41                            per_page: 100,
 42                        }});
 43
 44                        const filteredRepos = repos
 45                            .filter(repo => !repo.archived)
 46                            .filter(repo => {})
 47                            .map(repo => repo.name);
 48
 49                        console.log(`Found ${{filteredRepos.length}} extension repos`);
 50                        return filteredRepos;
 51                    "#},
 52                    exclusion_filter
 53                ),
 54            ))
 55            .add_with(("result-encoding", "json"));
 56
 57        let filtered_repos = StepOutput::new(&step, "result");
 58
 59        (step, filtered_repos)
 60    }
 61
 62    let (get_org_repositories, list_repos_output) = get_repositories();
 63
 64    let job = Job::default()
 65        .runs_on(runners::LINUX_SMALL)
 66        .timeout_minutes(5u32)
 67        .outputs([("repos".to_owned(), list_repos_output.to_string())])
 68        .add_step(get_org_repositories);
 69
 70    named::job(job)
 71}
 72
 73fn rollout_workflows_to_extension(fetch_repos_job: &NamedJob) -> NamedJob {
 74    fn checkout_zed_repo() -> Step<Use> {
 75        steps::checkout_repo()
 76            .name("checkout_zed_repo")
 77            .add_with(("path", "zed"))
 78    }
 79
 80    fn checkout_extension_repo(token: &StepOutput) -> Step<Use> {
 81        steps::checkout_repo_with_token(token)
 82            .add_with(("repository", "zed-extensions/${{ matrix.repo }}"))
 83            .add_with(("path", "extension"))
 84    }
 85
 86    fn copy_workflow_files() -> Step<Run> {
 87        named::bash(indoc! {r#"
 88            mkdir -p extension/.github/workflows
 89            cp zed/extensions/workflows/*.yml extension/.github/workflows/
 90        "#})
 91    }
 92
 93    fn get_short_sha() -> (Step<Run>, StepOutput) {
 94        let step = named::bash(indoc! {r#"
 95            echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
 96        "#})
 97        .id("short-sha")
 98        .working_directory("zed");
 99
100        let step_output = StepOutput::new(&step, "sha_short");
101
102        (step, step_output)
103    }
104
105    fn create_pull_request(token: &StepOutput, short_sha: StepOutput) -> Step<Use> {
106        let title = format!("Update CI workflows to zed@{}", short_sha);
107
108        named::uses("peter-evans", "create-pull-request", "v7")
109            .add_with(("path", "extension"))
110            .add_with(("title", title.clone()))
111            .add_with((
112                "body",
113                indoc! {r#"
114                    This PR updates the CI workflow files from the main Zed repository
115                    based on the commit zed-industries/zed@${{ github.sha }}
116                "#},
117            ))
118            .add_with(("commit-message", title))
119            .add_with(("branch", "update-workflows"))
120            .add_with((
121                "committer",
122                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
123            ))
124            .add_with((
125                "author",
126                "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>",
127            ))
128            .add_with(("base", "main"))
129            .add_with(("delete-branch", true))
130            .add_with(("token", token.to_string()))
131            .add_with(("sign-commits", true))
132            .id("create-pr")
133    }
134
135    fn enable_auto_merge(token: &StepOutput) -> Step<gh_workflow::Run> {
136        named::bash(indoc! {r#"
137            PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
138            if [ -n "$PR_NUMBER" ]; then
139                cd extension
140                gh pr merge "$PR_NUMBER" --auto --squash
141            fi
142        "#})
143        .add_env(("GH_TOKEN", token.to_string()))
144    }
145
146    let (authenticate, token) = steps::authenticate_as_zippy();
147    let (calculate_short_sha, short_sha) = get_short_sha();
148
149    let job = Job::default()
150        .needs([fetch_repos_job.name.clone()])
151        .cond(Expression::new(format!(
152            "needs.{}.outputs.repos != '[]'",
153            fetch_repos_job.name
154        )))
155        .runs_on(runners::LINUX_SMALL)
156        .timeout_minutes(10u32)
157        .strategy(
158            Strategy::default()
159                .fail_fast(false)
160                .max_parallel(5u32)
161                .matrix(json!({
162                    "repo": format!("${{{{ fromJson(needs.{}.outputs.repos) }}}}", fetch_repos_job.name)
163                })),
164        )
165        .add_step(authenticate)
166        .add_step(checkout_zed_repo())
167        .add_step(checkout_extension_repo(&token))
168        .add_step(copy_workflow_files())
169        .add_step(calculate_short_sha)
170        .add_step(create_pull_request(&token, short_sha))
171        .add_step(enable_auto_merge(&token));
172
173    named::job(job)
174}