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