extension_workflow_rollout.rs

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